Skip to content

Commit 73b06ff

Browse files
authored
refactor(audit,auth): functional options for NewUsageRecorder + NewInterceptor (#236)
Continues the functional-options refactor from #235. Both constructors gain With...() options and drop struct-config / positional-args style. - audit.NewUsageRecorder(store, opts...) — WithFlushInterval, WithLogger. RecorderConfig removed. - auth.NewInterceptor(ctx, jwksURL, opts...) — WithIssuer, WithLogger. The previous positional issuer/logger args are now options. auth uses the type name InterceptorOption so future constructors in the package can add their own options without conflict. Closes #233, #234.
1 parent 7c9acf5 commit 73b06ff

6 files changed

Lines changed: 104 additions & 56 deletions

File tree

cmd/server/main.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,10 @@ func run() int {
153153
// Auth interceptor.
154154
var authInterceptor server.GRPCInterceptor
155155
if cfg.JWTJWKSURL != "" {
156-
jwtInterceptor, jwtErr := auth.NewInterceptor(ctx, cfg.JWTJWKSURL, cfg.JWTIssuer, logger)
156+
jwtInterceptor, jwtErr := auth.NewInterceptor(ctx, cfg.JWTJWKSURL,
157+
auth.WithIssuer(cfg.JWTIssuer),
158+
auth.WithLogger(logger),
159+
)
157160
if jwtErr != nil {
158161
logger.ErrorContext(ctx, "failed to create auth interceptor", "error", jwtErr)
159162
return 1
@@ -212,10 +215,10 @@ func run() int {
212215
// Usage recorder — async batched read tracking for audit stats.
213216
var recorder *audit.UsageRecorder
214217
if cfg.UsageTrackingEnabled {
215-
recorder = audit.NewUsageRecorder(auditStoreVal, audit.RecorderConfig{
216-
FlushInterval: cfg.UsageFlushInterval,
217-
Logger: logger,
218-
})
218+
recorder = audit.NewUsageRecorder(auditStoreVal,
219+
audit.WithFlushInterval(cfg.UsageFlushInterval),
220+
audit.WithLogger(logger),
221+
)
219222
go recorder.Start(ctx)
220223
logger.InfoContext(ctx, "usage tracking enabled", "flush_interval", cfg.UsageFlushInterval)
221224
} else {

internal/audit/recorder.go

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,23 @@ import (
77
"time"
88
)
99

10-
// RecorderConfig holds configuration for the UsageRecorder.
11-
type RecorderConfig struct {
12-
FlushInterval time.Duration
13-
Logger *slog.Logger
10+
// Option configures a UsageRecorder.
11+
type Option func(*recorderOptions)
12+
13+
type recorderOptions struct {
14+
flushInterval time.Duration
15+
logger *slog.Logger
16+
}
17+
18+
// WithFlushInterval sets how often pending stats are flushed to the store.
19+
// Zero or negative falls back to 30s.
20+
func WithFlushInterval(d time.Duration) Option {
21+
return func(o *recorderOptions) { o.flushInterval = d }
22+
}
23+
24+
// WithLogger sets the recorder logger. Defaults to slog.Default() when unset.
25+
func WithLogger(l *slog.Logger) Option {
26+
return func(o *recorderOptions) { o.logger = l }
1427
}
1528

1629
// usageKey identifies a unique (tenant, field) pair for batching.
@@ -42,19 +55,24 @@ type UsageRecorder struct {
4255
}
4356

4457
// NewUsageRecorder creates a new recorder. Call Start to begin the background flush goroutine.
45-
func NewUsageRecorder(store Store, cfg RecorderConfig) *UsageRecorder {
46-
interval := cfg.FlushInterval
47-
if interval <= 0 {
48-
interval = 30 * time.Second
58+
func NewUsageRecorder(store Store, opts ...Option) *UsageRecorder {
59+
o := recorderOptions{
60+
flushInterval: 30 * time.Second,
61+
logger: slog.Default(),
62+
}
63+
for _, opt := range opts {
64+
opt(&o)
65+
}
66+
if o.flushInterval <= 0 {
67+
o.flushInterval = 30 * time.Second
4968
}
50-
logger := cfg.Logger
51-
if logger == nil {
52-
logger = slog.Default()
69+
if o.logger == nil {
70+
o.logger = slog.Default()
5371
}
5472
return &UsageRecorder{
5573
store: store,
56-
logger: logger,
57-
interval: interval,
74+
logger: o.logger,
75+
interval: o.flushInterval,
5876
pending: make(map[usageKey]*usageBucket),
5977
done: make(chan struct{}),
6078
}

internal/audit/recorder_test.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ import (
1717
var testRecorderLogger = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
1818

1919
func newTestRecorder(store *MemoryStore) *UsageRecorder {
20-
return NewUsageRecorder(store, RecorderConfig{
21-
FlushInterval: time.Hour, // manual flush in tests
22-
Logger: testRecorderLogger,
23-
})
20+
return NewUsageRecorder(store,
21+
WithFlushInterval(time.Hour), // manual flush in tests
22+
WithLogger(testRecorderLogger),
23+
)
2424
}
2525

2626
func TestRecordRead_SingleField(t *testing.T) {
@@ -151,10 +151,10 @@ func TestFlush_EmptyBuffer(t *testing.T) {
151151

152152
func TestFlush_StoreError(t *testing.T) {
153153
store := &failingStore{MemoryStore: NewMemoryStore(), err: errors.New("db down")}
154-
r := NewUsageRecorder(store, RecorderConfig{
155-
FlushInterval: time.Hour,
156-
Logger: testRecorderLogger,
157-
})
154+
r := NewUsageRecorder(store,
155+
WithFlushInterval(time.Hour),
156+
WithLogger(testRecorderLogger),
157+
)
158158

159159
r.RecordRead("t1", "app.fee", nil)
160160
err := r.Flush(context.Background())
@@ -173,10 +173,10 @@ func TestNilRecorder_SafeToCall(t *testing.T) {
173173

174174
func TestAutoFlush(t *testing.T) {
175175
store := NewMemoryStore()
176-
r := NewUsageRecorder(store, RecorderConfig{
177-
FlushInterval: 20 * time.Millisecond,
178-
Logger: testRecorderLogger,
179-
})
176+
r := NewUsageRecorder(store,
177+
WithFlushInterval(20*time.Millisecond),
178+
WithLogger(testRecorderLogger),
179+
)
180180

181181
ctx, cancel := context.WithCancel(context.Background())
182182
go r.Start(ctx)
@@ -200,10 +200,10 @@ func TestAutoFlush(t *testing.T) {
200200

201201
func TestStop_FinalFlush(t *testing.T) {
202202
store := NewMemoryStore()
203-
r := NewUsageRecorder(store, RecorderConfig{
204-
FlushInterval: time.Hour, // won't auto-flush
205-
Logger: testRecorderLogger,
206-
})
203+
r := NewUsageRecorder(store,
204+
WithFlushInterval(time.Hour), // won't auto-flush
205+
WithLogger(testRecorderLogger),
206+
)
207207

208208
ctx, cancel := context.WithCancel(context.Background())
209209
go r.Start(ctx)

internal/auth/jwt.go

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,33 @@ type Interceptor struct {
7070
logger *slog.Logger
7171
}
7272

73-
// NewInterceptor creates a new auth interceptor.
74-
// jwksURL is the JWKS endpoint for key discovery. issuer is optional.
75-
func NewInterceptor(ctx context.Context, jwksURL, issuer string, logger *slog.Logger) (*Interceptor, error) {
73+
// InterceptorOption configures a JWT Interceptor.
74+
type InterceptorOption func(*interceptorOptions)
75+
76+
type interceptorOptions struct {
77+
issuer string
78+
logger *slog.Logger
79+
}
80+
81+
// WithIssuer enforces an expected `iss` claim during JWT validation.
82+
// When unset, the issuer claim is not checked.
83+
func WithIssuer(issuer string) InterceptorOption {
84+
return func(o *interceptorOptions) { o.issuer = issuer }
85+
}
86+
87+
// WithLogger sets the interceptor logger. Defaults to slog.Default() when unset.
88+
func WithLogger(l *slog.Logger) InterceptorOption {
89+
return func(o *interceptorOptions) { o.logger = l }
90+
}
91+
92+
// NewInterceptor creates a new auth interceptor. jwksURL is the JWKS endpoint
93+
// for key discovery; pass WithIssuer / WithLogger for optional configuration.
94+
func NewInterceptor(ctx context.Context, jwksURL string, opts ...InterceptorOption) (*Interceptor, error) {
95+
o := interceptorOptions{logger: slog.Default()}
96+
for _, opt := range opts {
97+
opt(&o)
98+
}
99+
76100
jwksCtx, jwksCancel := context.WithCancel(ctx)
77101
jwks, err := keyfunc.NewDefaultCtx(jwksCtx, []string{jwksURL})
78102
if err != nil {
@@ -83,8 +107,8 @@ func NewInterceptor(ctx context.Context, jwksURL, issuer string, logger *slog.Lo
83107
return &Interceptor{
84108
jwks: jwks,
85109
jwksCancel: jwksCancel,
86-
issuer: issuer,
87-
logger: logger,
110+
issuer: o.issuer,
111+
logger: o.logger,
88112
}, nil
89113
}
90114

internal/auth/jwt_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ func newTestInterceptor(t *testing.T, issuer string) *Interceptor {
6868
t.Cleanup(srv.Close)
6969

7070
ctx := context.Background()
71-
interceptor, err := NewInterceptor(ctx, srv.URL, issuer, testLogger)
71+
interceptor, err := NewInterceptor(ctx, srv.URL,
72+
WithIssuer(issuer),
73+
WithLogger(testLogger),
74+
)
7275
require.NoError(t, err)
7376
t.Cleanup(interceptor.Close)
7477
return interceptor

internal/config/service_test.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -548,10 +548,10 @@ func TestGetField_RecordsUsage(t *testing.T) {
548548
store := &mockStore{}
549549
c := &mockCache{}
550550
auditStore := audit.NewMemoryStore()
551-
recorder := audit.NewUsageRecorder(auditStore, audit.RecorderConfig{
552-
FlushInterval: time.Hour,
553-
Logger: testLogger,
554-
})
551+
recorder := audit.NewUsageRecorder(auditStore,
552+
audit.WithFlushInterval(time.Hour),
553+
audit.WithLogger(testLogger),
554+
)
555555
svc := NewService(ServiceConfig{
556556
Store: store,
557557
Cache: c,
@@ -583,10 +583,10 @@ func TestGetConfig_RecordsUsage(t *testing.T) {
583583
store := &mockStore{}
584584
c := &mockCache{}
585585
auditStore := audit.NewMemoryStore()
586-
recorder := audit.NewUsageRecorder(auditStore, audit.RecorderConfig{
587-
FlushInterval: time.Hour,
588-
Logger: testLogger,
589-
})
586+
recorder := audit.NewUsageRecorder(auditStore,
587+
audit.WithFlushInterval(time.Hour),
588+
audit.WithLogger(testLogger),
589+
)
590590
svc := NewService(ServiceConfig{
591591
Store: store,
592592
Cache: c,
@@ -626,10 +626,10 @@ func TestGetFields_RecordsUsage(t *testing.T) {
626626
store := &mockStore{}
627627
c := &mockCache{}
628628
auditStore := audit.NewMemoryStore()
629-
recorder := audit.NewUsageRecorder(auditStore, audit.RecorderConfig{
630-
FlushInterval: time.Hour,
631-
Logger: testLogger,
632-
})
629+
recorder := audit.NewUsageRecorder(auditStore,
630+
audit.WithFlushInterval(time.Hour),
631+
audit.WithLogger(testLogger),
632+
)
633633
svc := NewService(ServiceConfig{
634634
Store: store,
635635
Cache: c,
@@ -665,10 +665,10 @@ func TestGetConfig_CacheHit_RecordsUsage(t *testing.T) {
665665
store := &mockStore{}
666666
c := &mockCache{}
667667
auditStore := audit.NewMemoryStore()
668-
recorder := audit.NewUsageRecorder(auditStore, audit.RecorderConfig{
669-
FlushInterval: time.Hour,
670-
Logger: testLogger,
671-
})
668+
recorder := audit.NewUsageRecorder(auditStore,
669+
audit.WithFlushInterval(time.Hour),
670+
audit.WithLogger(testLogger),
671+
)
672672
svc := NewService(ServiceConfig{
673673
Store: store,
674674
Cache: c,

0 commit comments

Comments
 (0)