wideslog

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 1, 2026 License: MIT Imports: 6 Imported by: 0

README

wideslog

PkgGoDev

Wide events for Go, built on top of log/slog.

wideslog collects logs from one operation and emits one structured record. Shared context is written once at the root; individual steps remain available inside events.

Payment API example

One payment request logs five steps. Each standard slog record repeats the full request context (service, request_id, tenant_id, user_id):

{"time":"2026-08-30T09:15:42.100Z","level":"INFO","msg":"request received","service":"payments-api","request_id":"req_01J9Y7K2","tenant_id":"tenant_acme","user_id":"usr_4821","method":"POST","path":"/v1/payments"}
{"time":"2026-08-30T09:15:42.101Z","level":"INFO","msg":"customer loaded","service":"payments-api","request_id":"req_01J9Y7K2","tenant_id":"tenant_acme","user_id":"usr_4821","customer_id":"cus_42A19C","customer_plan":"pro"}
{"time":"2026-08-30T09:15:42.103Z","level":"INFO","msg":"payment method verified","service":"payments-api","request_id":"req_01J9Y7K2","tenant_id":"tenant_acme","user_id":"usr_4821","brand":"visa","last4":"4242","risk_score":12}
{"time":"2026-08-30T09:15:42.105Z","level":"INFO","msg":"payout authorized","service":"payments-api","request_id":"req_01J9Y7K2","tenant_id":"tenant_acme","user_id":"usr_4821","payout_id":"pay_7D91B4","amount_cents":12990,"currency":"BRL"}
{"time":"2026-08-30T09:15:42.108Z","level":"INFO","msg":"payment completed","service":"payments-api","request_id":"req_01J9Y7K2","tenant_id":"tenant_acme","user_id":"usr_4821","payment_id":"pay_7D91B4","status":"confirmed"}

The same request through wideslog emits one record. NewEvent names the operation on the root; the shared context is written once as root attributes; each step keeps only its own fields inside events:

{"time":"2026-08-30T09:15:42.108Z","level":"INFO","msg":"charge payment pay_7D91B4","timestamp":"2026-08-30T09:15:42.100Z","duration_ms":8,"event_count":5,"service":"payments-api","request_id":"req_01J9Y7K2","tenant_id":"tenant_acme","user_id":"usr_4821","events":[{"offset_ms":0,"level":"INFO","msg":"request received","method":"POST","path":"/v1/payments"},{"offset_ms":12,"level":"INFO","msg":"customer loaded","customer_id":"cus_42A19C","customer_plan":"pro"},{"offset_ms":32,"level":"INFO","msg":"payment method verified","brand":"visa","last4":"4242","risk_score":12},{"offset_ms":54,"level":"INFO","msg":"payout authorized","payout_id":"pay_7D91B4","amount_cents":12990,"currency":"BRL"},{"offset_ms":78,"level":"INFO","msg":"payment completed","payment_id":"pay_7D91B4","status":"confirmed"}]}

Five standard records become one line. Request metadata and the operation name are written once, while event-specific attributes stay with the event that produced them.

Savings simulation

Serialized as compact JSON with one trailing newline per record, the payment request above produces five records of 214, 224, 229, 240, and 224 bytes — 1,131 B per request. The wideslog output is one record of 802 bytes: 329 B saved per request (29.1%), before compression, indexing, or transport overhead.

Scenario Standard wideslog Byte savings
Payment API 5 lines / 1,131 B 1 line / 802 B 329 B (29.1%)

Using 86,400 seconds per day, 30 days per month, and 365 days per year at 50 requests per second:

Period Standard wideslog Bytes saved
day 4.89 GB 3.46 GB 1.42 GB
month 146.58 GB 103.94 GB 42.64 GB
year 1,783.36 GB 1,264.59 GB 518.77 GB

These are simulations, not universal benchmarks. Savings depend on event count, repeated context, field sizes, handler options, compression, and backend pricing. The line reduction is deterministic: five standard records become one wide record per operation. The byte savings come mostly from writing the shared request context and the per-line time/level/msg fields only once, at the root.

Quick start

Create the logger once during application startup:

handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
})
logger := wideslog.New(handler)

Or use the JSON convenience constructor:

logger := wideslog.JSONHandler(os.Stdout, nil)

Create one event per request or operation:

func process(ctx context.Context, logger *slog.Logger) error {
    ctx, event := wideslog.NewEvent(ctx, logger, "checkout order "+orderID)
    defer event.End(ctx)

    event.Add(
        slog.String("request_id", "req_01J8X7"),
        slog.String("tenant_id", "tenant_acme"),
    )

    logger.InfoContext(ctx, "customer loaded", "customer_id", "cus_42A19C")
    logger.InfoContext(ctx, "payment authorized", "amount_cents", 12990)
    return nil
}

The message passed to NewEvent identifies the operation and becomes the message of the root record. Event.Add writes root attributes. Attributes passed to a log call remain on that event; steps logged through the logger appear as entries in events. Logs without an active event pass through to the wrapped handler.

Timestamp modes

The root record always includes timestamp and duration_ms. Configure only the fields inside events:

ctx, event := wideslog.NewEvent(ctx, logger, "operation completed",
    wideslog.WithTimestampMode(wideslog.TimestampNone),
)

timestamp marks when the operation started. In the examples above the root time and level fields (which slog always emits) are omitted for readability; time is when the wide record was written.

Available modes:

  • TimestampNone: no timestamp on individual events.
  • TimestampAbsolute: an ISO-8601 timestamp on each event.
  • TimestampOffset: an elapsed offset_ns, offset_us, or offset_ms.

The default is TimestampOffset with OffsetMicroseconds:

ctx, event := wideslog.NewEvent(ctx, logger, "operation completed",
    wideslog.WithTimestampMode(wideslog.TimestampOffset),
    wideslog.WithOffsetUnit(wideslog.OffsetMilliseconds),
)

logger.InfoContext(ctx, "started")
time.Sleep(25 * time.Millisecond)
logger.InfoContext(ctx, "finished")
event.End(ctx)

HTTP middleware

Create the event from the request context and pass the returned context forward:

func loggingMiddleware(logger *slog.Logger, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx, event := wideslog.NewEvent(r.Context(), logger, "request completed")
        defer event.End(ctx)

        next.ServeHTTP(w, r.WithContext(ctx))
        event.Add(slog.Int("http.status", 200))
    })
}

Each request receives its own event. The logger can be shared; request state is stored in the context and the Event.

When not to use it

Use standard slog when each line must be independently searchable or when an operation runs for a long time without useful checkpoints. Wide events are a summary of an operation, not a replacement for traces or fine-grained debug logs.

Example

See example/README.md and run:

go run ./example

Inspiration

wideslog is inspired by Wide Events, written by my friend Luiz Dubiela.

License

This project is licensed under the MIT License.

Documentation

Overview

Package wideslog collects slog records into request-scoped wide events.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func JSONHandler

func JSONHandler(
	w io.Writer,
	opts *slog.HandlerOptions,
) *slog.Logger

JSONHandler returns a logger backed by slog's JSONHandler.

func New

func New(next slog.Handler) *slog.Logger

New returns a logger backed by a wide-event Handler.

Types

type Config

type Config struct {
	TimestampMode TimestampMode
	OffsetUnit    OffsetUnit
}

Config defines the timestamp behavior of an Event.

func NewConfig

func NewConfig(options ...Option) Config

NewConfig returns the default configuration with options applied.

type Event

type Event struct {
	// contains filtered or unexported fields
}

Event collects request-scoped slog records into one wide event.

An Event is safe for concurrent use. This is useful when multiple goroutines contribute logs to the same request.

func FromContext

func FromContext(ctx context.Context) *Event

FromContext returns the Event stored in ctx, or nil when none is present.

func NewEvent

func NewEvent(
	ctx context.Context,
	logger *slog.Logger,
	msg string,
	options ...Option,
) (context.Context, *Event)

NewEvent begins collecting records for a request-scoped wide event.

msg identifies the operation and becomes the message of the root wide record. The returned context contains the Event. Any slog call made with this context is captured by a wideslog Handler.

NewEvent panics when logger is not backed by a wideslog Handler (use wideslog.New or wideslog.JSONHandler). A logger created with plain slog would silently emit records instead of buffering them.

NewEvent must be called on a context without an existing Event. Calling it twice stores the inner event in the context and the outer event stops collecting records.

func (*Event) Add

func (e *Event) Add(attrs ...slog.Attr)

Add attaches attributes to the root of the final wide event.

These attributes are not added to individual events.

func (*Event) End

func (e *Event) End(ctx context.Context)

End emits the accumulated wide event.

End is idempotent. Only the first call emits the event, and no error is returned: an output handler that fails to write is treated the same way slog treats handler errors, silently.

The root record carries the message passed to NewEvent. Steps logged through the event's context, including the final one, live in the events field. The root record uses the highest level found in the buffered steps (Info when there are none), so handlers that only accept serious levels still receive it.

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler buffers slog records when an Event is present in the context.

When no Event is present, it behaves like the wrapped slog.Handler.

func NewHandler

func NewHandler(next slog.Handler) *Handler

NewHandler returns a Handler that wraps next.

func (*Handler) Enabled

func (h *Handler) Enabled(
	ctx context.Context,
	level slog.Level,
) bool

Enabled reports whether the wrapped handler accepts level.

This preserves normal slog filtering behavior.

func (*Handler) Handle

func (h *Handler) Handle(
	ctx context.Context,
	record slog.Record,
) error

Handle buffers record in the active Event or forwards it to the wrapped handler when no Event exists.

Levels the wrapped handler rejects are not buffered; filtering is enforced when the record arrives, matching what slog would do without an Event.

func (*Handler) WithAttrs

func (h *Handler) WithAttrs(
	attrs []slog.Attr,
) slog.Handler

WithAttrs returns a child handler with attrs attached to subsequent records.

The parent handler is never modified, matching slog.Handler semantics.

func (*Handler) WithGroup

func (h *Handler) WithGroup(name string) slog.Handler

WithGroup returns a child handler that nests subsequent attributes under name.

Groups are scoped to this logger and its descendants.

type OffsetUnit

type OffsetUnit uint8

OffsetUnit selects the unit used for relative event timestamps.

const (
	// OffsetNanoseconds records offsets in nanoseconds.
	OffsetNanoseconds OffsetUnit = iota

	// OffsetMicroseconds records offsets in microseconds.
	OffsetMicroseconds

	// OffsetMilliseconds records offsets in milliseconds.
	OffsetMilliseconds
)

func (OffsetUnit) String

func (u OffsetUnit) String() string

type Option

type Option func(*Config)

Option configures an Event created by Start.

func WithOffsetUnit

func WithOffsetUnit(unit OffsetUnit) Option

WithOffsetUnit sets the unit used when TimestampOffset is enabled.

func WithTimestampMode

func WithTimestampMode(mode TimestampMode) Option

WithTimestampMode sets how individual event timestamps are represented.

type TimestampMode

type TimestampMode uint8

TimestampMode controls how timestamps are recorded for individual events.

const (
	// TimestampNone omits timestamps from individual events.
	TimestampNone TimestampMode = iota

	// TimestampAbsolute records each event's wall-clock timestamp.
	TimestampAbsolute

	// TimestampOffset records each event's elapsed time from the wide event start.
	TimestampOffset
)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL