forked from siderolabs/omni
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: add rotating log for audit data
Adds rotating audit log writer. Also minor improvements. For siderolabs#37 Signed-off-by: Dmitriy Matrenichev <[email protected]>
- Loading branch information
Showing
11 changed files
with
264 additions
and
180 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// Copyright (c) 2024 Sidero Labs, Inc. | ||
// | ||
// Use of this software is governed by the Business Source License | ||
// included in the LICENSE file. | ||
|
||
// Package audit provides a state wrapper that logs audit events. | ||
package audit | ||
|
||
import ( | ||
"github.com/siderolabs/omni/internal/pkg/auth/role" | ||
) | ||
|
||
// Data contains the audit data. | ||
type Data struct { | ||
UserAgent string `json:"user_agent,omitempty"` | ||
IPAddress string `json:"ip_address,omitempty"` | ||
UserID string `json:"user_id,omitempty"` | ||
Identity string `json:"identity,omitempty"` | ||
Role role.Role `json:"role,omitempty"` | ||
Email string `json:"email,omitempty"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// Copyright (c) 2024 Sidero Labs, Inc. | ||
// | ||
// Use of this software is governed by the Business Source License | ||
// included in the LICENSE file. | ||
|
||
package audit | ||
|
||
import "time" | ||
|
||
func (l *LogFile) WriteAt(data any, at time.Time) error { | ||
return l.writeAt(data, at) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
// Copyright (c) 2024 Sidero Labs, Inc. | ||
// | ||
// Use of this software is governed by the Business Source License | ||
// included in the LICENSE file. | ||
|
||
package audit | ||
|
||
import ( | ||
"encoding/json" | ||
"os" | ||
"path/filepath" | ||
"runtime" | ||
"sync" | ||
"time" | ||
|
||
"github.com/siderolabs/gen/pair/ordered" | ||
) | ||
|
||
// LogFile is a rotating log file. | ||
// | ||
//nolint:govet | ||
type LogFile struct { | ||
dir string | ||
|
||
mu sync.Mutex | ||
f *os.File | ||
lastWrite time.Time | ||
} | ||
|
||
// NewLogFile creates a new rotating log file. | ||
func NewLogFile(dir string) *LogFile { | ||
return &LogFile{dir: dir} | ||
} | ||
|
||
// Write writes data to the log file, creating new one on demand. | ||
func (l *LogFile) Write(data any) error { | ||
return l.writeAt(data, time.Now()) | ||
} | ||
|
||
func (l *LogFile) writeAt(data any, at time.Time) error { | ||
f, err := l.openFile(at) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if err := json.NewEncoder(f).Encode(data); err != nil { | ||
return err | ||
} | ||
|
||
l.mu.Lock() | ||
l.lastWrite = at | ||
l.mu.Unlock() | ||
|
||
return nil | ||
} | ||
|
||
// openFile opens a file for the given date. It returns the file is date for at matches | ||
// the last write date. Otherwise, it opens a new file. | ||
func (l *LogFile) openFile(at time.Time) (*os.File, error) { | ||
l.mu.Lock() | ||
defer l.mu.Unlock() | ||
|
||
// TODO(Dmitriy): decide what to do with when we write entry for the previous day | ||
// into the current day (happens on day's border). One solution is to have two decriptors opened: | ||
// one for the current day and one for the previous day. We can close the previous day's descriptor | ||
// when we open the 3rd day's descriptor. This way we will have at most two descriptors opened. | ||
if l.f != nil && ordered.MakeTriple(at.Date()).Compare(ordered.MakeTriple(l.lastWrite.Date())) <= 0 { | ||
return l.f, nil | ||
} | ||
|
||
logPath := filepath.Join(l.dir, at.Format("2006-01-02")) + ".jsonlog" | ||
|
||
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
l.f = f | ||
|
||
// We are not closing files ourselves and this is intentional. The reason is: many writers can come to write | ||
// audit data at the day's border so, it's possible that instead of 1(old) -> 2(old) -> 3(old) -> new_file -> 4 -> 5 -> 6 | ||
// the order of operations will be 1(old) -> 2(old) -> new_file -> 3(old) -> 4 -> 5 -> 6. In this case, we don't | ||
// want goroutine to observe closed file descriptor. | ||
runtime.SetFinalizer(f, (*os.File).Close) | ||
|
||
return f, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
// Copyright (c) 2024 Sidero Labs, Inc. | ||
// | ||
// Use of this software is governed by the Business Source License | ||
// included in the LICENSE file. | ||
|
||
package audit_test | ||
|
||
import ( | ||
"embed" | ||
"io/fs" | ||
"os" | ||
"path/filepath" | ||
"runtime" | ||
"testing" | ||
"time" | ||
|
||
"github.com/siderolabs/gen/xtesting/must" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/siderolabs/omni/internal/backend/runtime/omni/audit" | ||
) | ||
|
||
//go:embed testdata/currentday | ||
var currentDay embed.FS | ||
|
||
func TestLogFile_CurrentDay(t *testing.T) { | ||
dir := must.Value(os.MkdirTemp("", "log_file_test"))(t) | ||
|
||
t.Cleanup(func() { os.RemoveAll(dir) }) //nolint:errcheck | ||
|
||
entries := []entry{ | ||
{shift: time.Second, data: audit.Data{UserAgent: "Mozilla/5.0", IPAddress: "10.10.0.1", Email: "[email protected]"}}, | ||
{shift: time.Minute, data: audit.Data{UserAgent: "Mozilla/5.0", IPAddress: "10.10.0.2", Email: "[email protected]"}}, | ||
{shift: 30 * time.Minute, data: audit.Data{UserAgent: "Mozilla/5.0", IPAddress: "10.10.0.3", Email: "[email protected]"}}, | ||
} | ||
|
||
start := time.Date(2012, 1, 1, 23, 0, 0, 0, time.Local) | ||
now := start | ||
file := audit.NewLogFile(dir) | ||
|
||
for _, e := range entries { | ||
now = now.Add(e.shift) | ||
|
||
require.NoError(t, file.WriteAt(e.data, now)) | ||
} | ||
|
||
checkFiles(t, dir, currentDay, "currentday") | ||
|
||
for range 100 { | ||
runtime.GC() // ensure file is collected | ||
} | ||
} | ||
|
||
//go:embed testdata/nextday | ||
var nextDay embed.FS | ||
|
||
func TestLogFile_CurrentAndNewDay(t *testing.T) { | ||
dir := must.Value(os.MkdirTemp("", "log_file_test"))(t) | ||
|
||
t.Cleanup(func() { os.RemoveAll(dir) }) //nolint:errcheck | ||
|
||
entries := []entry{ | ||
{shift: 0, data: audit.Data{UserAgent: "Mozilla/5.0", IPAddress: "10.10.0.1", Email: "[email protected]"}}, | ||
{shift: 55 * time.Minute, data: audit.Data{UserAgent: "Mozilla/5.0", IPAddress: "10.10.0.2", Email: "[email protected]"}}, | ||
{shift: 5 * time.Minute, data: audit.Data{UserAgent: "Mozilla/5.0", IPAddress: "10.10.0.3", Email: "[email protected]"}}, | ||
} | ||
|
||
start := time.Date(2012, 1, 1, 23, 0, 0, 0, time.Local) | ||
now := start | ||
file := audit.NewLogFile(dir) | ||
|
||
for _, e := range entries { | ||
now = now.Add(e.shift) | ||
|
||
require.NoError(t, file.WriteAt(e.data, now)) | ||
} | ||
|
||
checkFiles(t, dir, nextDay, "nextday") | ||
} | ||
|
||
//nolint:govet | ||
type entry struct { | ||
shift time.Duration | ||
data audit.Data | ||
} | ||
|
||
type subFS interface { | ||
fs.ReadFileFS | ||
fs.ReadDirFS | ||
} | ||
|
||
func checkFiles(t *testing.T, dir string, subFs subFS, folder string) { | ||
subFs = must.Value(fs.Sub(subFs, filepath.Join("testdata", folder)))(t).(subFS) //nolint:errcheck | ||
files := must.Value(subFs.ReadDir("."))(t) | ||
|
||
for _, f := range files { | ||
if f.IsDir() { | ||
t.Fatal("unexpected directory", f.Name()) | ||
} | ||
|
||
expectedData := string(must.Value(subFs.ReadFile(f.Name()))(t)) | ||
actualData := string(must.Value(os.ReadFile(filepath.Join(dir, f.Name())))(t)) | ||
|
||
require.Equal(t, expectedData, actualData, "file %s", f.Name()) | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
internal/backend/runtime/omni/audit/testdata/currentday/2012-01-01.jsonlog
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.1","email":"[email protected]"} | ||
{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.2","email":"[email protected]"} | ||
{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.3","email":"[email protected]"} |
2 changes: 2 additions & 0 deletions
2
internal/backend/runtime/omni/audit/testdata/nextday/2012-01-01.jsonlog
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.1","email":"[email protected]"} | ||
{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.2","email":"[email protected]"} |
1 change: 1 addition & 0 deletions
1
internal/backend/runtime/omni/audit/testdata/nextday/2012-01-02.jsonlog
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"user_agent":"Mozilla/5.0","ip_address":"10.10.0.3","email":"[email protected]"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters