Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ linters:
- goconst
- gosec
- nilnil
- noctx

- path: database_test\.go
linters:
Expand Down
10 changes: 9 additions & 1 deletion docs/src/content/docs/packages/log.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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
Expand Down
76 changes: 75 additions & 1 deletion log/wideevent_logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"log/slog"
"slices"
"time"
)

// WideEventLogger writes wide events with tail sampling.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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)
Expand Down
Loading