diff --git a/.golangci.yml b/.golangci.yml index 016f2c8..6dd7640 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -86,6 +86,7 @@ linters: - goconst - gosec - nilnil + - noctx - path: database_test\.go linters: diff --git a/docs/src/content/docs/packages/log.mdx b/docs/src/content/docs/packages/log.mdx index 1547a6f..b7053ce 100644 --- a/docs/src/content/docs/packages/log.mdx +++ b/docs/src/content/docs/packages/log.mdx @@ -13,7 +13,7 @@ Core Components: - `New`: Builds a text or JSON logger that automatically extracts values like `traceId` and `serviceName` from `context.Context`. - `TraceIDMiddleware`: Adds a per-request trace ID to context and response headers. - `Event`: Mutable wide-event model with attrs, steps, errors, severity, and duration. -- `WideEventLogger`: Writes finalized `Event` values through a `Sampler`. +- `WideEventLogger`: Writes finalized `Event` values through a `Sampler` and can also be installed as the package default logger for `Debug`/`Info`/`Warn`/`Error` calls. - `WideEventMiddleware`: Creates request-wide events, stores them in context, and emits them after handlers finish. - `Sampler`, `SamplerFunc`, `DefaultSampler`: Tail-sampling rules for keeping errors, slow requests, selected status codes, and random samples. - `EventFromContext`: Fetches the current request-wide event from context using `WideEventKey`. @@ -56,6 +56,14 @@ Core Components: This keeps all error events, slow requests (>=2s), `5xx` responses, and 5% of the remaining traffic. + You can also reuse the same `wideLogger` as the package default logger: + + ```go + log.SetDefault(wideLogger) + ``` + + In that mode, `log.Info(...)` and related calls emit a short-lived event that still goes through the configured sampler, just like explicit wide events created via middleware or `WriteEvent(...)`. + 4. Enrich events and logs inside handlers ```go diff --git a/log/wideevent_logger.go b/log/wideevent_logger.go index f3d8ccf..fef2503 100644 --- a/log/wideevent_logger.go +++ b/log/wideevent_logger.go @@ -5,6 +5,7 @@ import ( "io" "log/slog" "slices" + "time" ) // WideEventLogger writes wide events with tail sampling. @@ -14,6 +15,12 @@ type WideEventLogger struct { reservedAttrKeys []string } +const ( + simpleLogEventName = "log.record" +) + +var _ logger = (*WideEventLogger)(nil) + // NewWideEventLogger creates a wide-event logger. func NewWideEventLogger(w io.Writer, s Sampler, loggerType string, contextKeys map[string]any) *WideEventLogger { // If no sampler provided, use a keep-all sampler to prevent nil panics @@ -24,7 +31,10 @@ func NewWideEventLogger(w io.Writer, s Sampler, loggerType string, contextKeys m opts := &slog.HandlerOptions{ Level: slog.LevelDebug, ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { - if a.Key == slog.TimeKey || a.Key == slog.MessageKey { + if a.Key == slog.TimeKey { + return slog.Attr{} + } + if a.Key == slog.MessageKey && a.Value.Kind() == slog.KindString && a.Value.String() == "" { return slog.Attr{} } return a @@ -45,6 +55,46 @@ func NewWideEventLogger(w io.Writer, s Sampler, loggerType string, contextKeys m } } +// Debug logs a message at Debug level. +func (l *WideEventLogger) Debug(msg string, args ...any) { + l.DebugContext(context.Background(), msg, args...) +} + +// Info logs a message at Info level. +func (l *WideEventLogger) Info(msg string, args ...any) { + l.InfoContext(context.Background(), msg, args...) +} + +// Warn logs a message at Warn level. +func (l *WideEventLogger) Warn(msg string, args ...any) { + l.WarnContext(context.Background(), msg, args...) +} + +// Error logs a message at Error level. +func (l *WideEventLogger) Error(msg string, args ...any) { + l.ErrorContext(context.Background(), msg, args...) +} + +// DebugContext logs a message at Debug level with context. +func (l *WideEventLogger) DebugContext(ctx context.Context, msg string, args ...any) { + l.writeSimpleLog(ctx, slog.LevelDebug, msg, args...) +} + +// InfoContext logs a message at Info level with context. +func (l *WideEventLogger) InfoContext(ctx context.Context, msg string, args ...any) { + l.writeSimpleLog(ctx, slog.LevelInfo, msg, args...) +} + +// WarnContext logs a message at Warn level with context. +func (l *WideEventLogger) WarnContext(ctx context.Context, msg string, args ...any) { + l.writeSimpleLog(ctx, slog.LevelWarn, msg, args...) +} + +// ErrorContext logs a message at Error level with context. +func (l *WideEventLogger) ErrorContext(ctx context.Context, msg string, args ...any) { + l.writeSimpleLog(ctx, slog.LevelError, msg, args...) +} + // WriteEvent finalizes event duration and conditionally writes it. func (l *WideEventLogger) WriteEvent(ctx context.Context, e *Event) { e.Finish() @@ -54,6 +104,30 @@ func (l *WideEventLogger) WriteEvent(ctx context.Context, e *Event) { } } +func (l *WideEventLogger) writeSimpleLog(ctx context.Context, level slog.Level, msg string, args ...any) { + event := NewEvent(simpleLogEventName) + event.SetLevel(level) + event.AddAttrs(simpleLogEventAttrs(args...)) + event.Finish() + + if l.sampler.ShouldSample(ctx, event) { + l.logger.LogAttrs(ctx, event.Level(), msg, event.toAttrs(l.reservedAttrKeys)...) + } +} + +func simpleLogEventAttrs(args ...any) map[string]any { + attrs := map[string]any{} + + record := slog.NewRecord(time.Time{}, slog.LevelInfo, "", 0) + record.Add(args...) + record.Attrs(func(attr slog.Attr) bool { + attrs[attr.Key] = attr.Value + return true + }) + + return attrs +} + func wideEventReservedAttrKeys(contextKeys map[string]any) []string { reservedAttrKeys := append([]string{}, wideEventBuiltinAttrKeys()...) reservedAttrKeys = appendUnique(reservedAttrKeys, slog.LevelKey)