diff --git a/enterprise/audit/audittest/rand.go b/enterprise/audit/audittest/rand.go index 271ac98ed019c..eb7b624249146 100644 --- a/enterprise/audit/audittest/rand.go +++ b/enterprise/audit/audittest/rand.go @@ -23,12 +23,13 @@ func RandomLog() database.AuditLog { IPNet: *inet, Valid: true, }, - UserAgent: sql.NullString{String: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36", Valid: true}, - ResourceType: database.ResourceTypeOrganization, - ResourceID: uuid.New(), - ResourceTarget: "colin's organization", - Action: database.AuditActionDelete, - Diff: []byte("{}"), - StatusCode: http.StatusNoContent, + UserAgent: sql.NullString{String: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36", Valid: true}, + ResourceType: database.ResourceTypeOrganization, + ResourceID: uuid.New(), + ResourceTarget: "colin's organization", + Action: database.AuditActionDelete, + Diff: []byte("{}"), + StatusCode: http.StatusNoContent, + AdditionalFields: []byte("{}"), } } diff --git a/enterprise/audit/backends/slog.go b/enterprise/audit/backends/slog.go index 298310448e9cb..4506357475626 100644 --- a/enterprise/audit/backends/slog.go +++ b/enterprise/audit/backends/slog.go @@ -2,8 +2,10 @@ package backends import ( "context" + "database/sql" "github.com/fatih/structs" + "github.com/sqlc-dev/pqtype" "cdr.dev/slog" "github.com/coder/coder/v2/coderd/database" @@ -15,14 +17,14 @@ type slogBackend struct { } func NewSlog(logger slog.Logger) audit.Backend { - return slogBackend{log: logger} + return &slogBackend{log: logger} } -func (slogBackend) Decision() audit.FilterDecision { +func (*slogBackend) Decision() audit.FilterDecision { return audit.FilterDecisionExport } -func (b slogBackend) Export(ctx context.Context, alog database.AuditLog) error { +func (b *slogBackend) Export(ctx context.Context, alog database.AuditLog) error { // We don't use structs.Map because we don't want to recursively convert // fields into maps. When we keep the type information, slog can more // pleasantly format the output. For example, the clean result of @@ -30,9 +32,22 @@ func (b slogBackend) Export(ctx context.Context, alog database.AuditLog) error { sfs := structs.Fields(alog) var fields []any for _, sf := range sfs { - fields = append(fields, slog.F(sf.Name(), sf.Value())) + fields = append(fields, b.fieldToSlog(sf)) } b.log.Info(ctx, "audit_log", fields...) return nil } + +func (*slogBackend) fieldToSlog(field *structs.Field) slog.Field { + val := field.Value() + + switch ty := field.Value().(type) { + case pqtype.Inet: + val = ty.IPNet.IP.String() + case sql.NullString: + val = ty.String + } + + return slog.F(field.Name(), val) +} diff --git a/enterprise/audit/backends/slog_test.go b/enterprise/audit/backends/slog_test.go index 03ea27ac524eb..ff42779a773ab 100644 --- a/enterprise/audit/backends/slog_test.go +++ b/enterprise/audit/backends/slog_test.go @@ -1,13 +1,24 @@ package backends_test import ( + "bytes" "context" + "database/sql" + "encoding/json" + "net" + "net/http" "testing" + "time" "github.com/fatih/structs" + "github.com/google/uuid" + "github.com/sqlc-dev/pqtype" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogjson" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/enterprise/audit/audittest" "github.com/coder/coder/v2/enterprise/audit/backends" ) @@ -34,6 +45,54 @@ func TestSlogBackend(t *testing.T) { require.Equal(t, sink.entries[0].Message, "audit_log") require.Len(t, sink.entries[0].Fields, len(structs.Fields(alog))) }) + + t.Run("FormatsCorrectly", func(t *testing.T) { + t.Parallel() + + var ( + ctx, cancel = context.WithCancel(context.Background()) + + buf = bytes.NewBuffer(nil) + logger = slog.Make(slogjson.Sink(buf)) + backend = backends.NewSlog(logger) + + _, inet, _ = net.ParseCIDR("127.0.0.1/32") + alog = database.AuditLog{ + ID: uuid.UUID{1}, + Time: time.Unix(1257894000, 0), + UserID: uuid.UUID{2}, + OrganizationID: uuid.UUID{3}, + Ip: pqtype.Inet{ + IPNet: *inet, + Valid: true, + }, + UserAgent: sql.NullString{String: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36", Valid: true}, + ResourceType: database.ResourceTypeOrganization, + ResourceID: uuid.UUID{4}, + ResourceTarget: "colin's organization", + ResourceIcon: "photo.png", + Action: database.AuditActionDelete, + Diff: []byte(`{"1": 2}`), + StatusCode: http.StatusNoContent, + AdditionalFields: []byte(`{"name":"doug","species":"cat"}`), + RequestID: uuid.UUID{5}, + } + ) + defer cancel() + + err := backend.Export(ctx, alog) + require.NoError(t, err) + logger.Sync() + + s := struct { + Fields json.RawMessage `json:"fields"` + }{} + err = json.Unmarshal(buf.Bytes(), &s) + require.NoError(t, err) + + expected := `{"ID":"01000000-0000-0000-0000-000000000000","Time":"2009-11-10T23:00:00Z","UserID":"02000000-0000-0000-0000-000000000000","OrganizationID":"03000000-0000-0000-0000-000000000000","Ip":"127.0.0.1","UserAgent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","ResourceType":"organization","ResourceID":"04000000-0000-0000-0000-000000000000","ResourceTarget":"colin's organization","Action":"delete","Diff":{"1":2},"StatusCode":204,"AdditionalFields":{"name":"doug","species":"cat"},"RequestID":"05000000-0000-0000-0000-000000000000","ResourceIcon":"photo.png"}` + assert.Equal(t, expected, string(s.Fields)) + }) } type fakeSink struct {