Help us shape the future of Go! Take the 2026 Go Developer Survey

wideevent

package module
v0.3.3 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 2026 License: MIT Imports: 14 Imported by: 0

README

go-wide-event-logger

A Go library for wide event logging — one rich structured log line per unit of work instead of dozens of scattered log.Info/Error/Debug calls.

What are Wide Events?

Traditional logging produces disconnected log lines:

INFO  Starting user update
DEBUG Fetching user from cache
DEBUG Cache miss, querying database
INFO  User updated successfully
DEBUG Broadcasting websocket event

Wide events replace this with one comprehensive event containing all context:

{
  "level": "info",
  "message": "wide_event",
  "name": "update_user",
  "outcome": "success",
  "request": { "method": "PUT", "path": "/api/v1/users/abc-123" },
  "response": { "status": 200, "latency_ms": 12.4 },
  "user": { "id": "def-456", "name": "john", "role": "admin" },
  "db": { "operation": "update", "entity": "user" },
  "cache": { "hit": false }
}

One event per request. Every field queryable. No context lost.

Installation

go get github.com/mxcd/go-wide-event-logger

Quick Start

import wideevent "github.com/mxcd/go-wide-event-logger"

func main() {
    r := gin.Default()
    r.Use(wideevent.Middleware())

    r.GET("/api/users", func(c *gin.Context) {
        we := wideevent.FromGin(c)
        we.Str("endpoint", "list_users").Success()
        c.JSON(200, users)
    })

    r.Run(":8080")
}

Convention: the we Variable

Wide events use the variable name we — it reads as natural English:

we.Success()           // "we succeeded"
we.Failure(err)        // "we failed with err"
we.UserID(id)          // "we have user ID"
we.UserName("john")    // "we have user name john"

API Reference

Event Creation
Function Use Case
wideevent.FromGin(c) Get event from Gin handler (created by middleware)
wideevent.FromContext(ctx) Get event from standard context (repository layers)
wideevent.Begin(emitter, name) Create standalone event (background tasks, cron jobs)
Typed Setters

All setters return *Event for chaining:

we.Str("key", "value")           // string
we.Int("key", 42)                // int
we.Int64("key", int64(99))       // int64
we.Float64("key", 3.14)          // float64
we.Bool("key", true)             // bool
we.Dur("key", 5*time.Second)     // time.Duration (emitted as ms)
we.Time("key", time.Now())       // time.Time (emitted as RFC3339Nano)
we.Err("key", err)               // error (nil is a no-op)
we.Interface("key", anything)    // any
we.UUID("key", uuidValue)        // fmt.Stringer
we.Set("key", anyValue)          // generic setter
Convenience Builders
we.Success()              // sets outcome = "success"
we.Failure(err)           // sets outcome = "failure", error = err.Error()
we.UserID(id)             // sets user.id
we.UserName("john")       // sets user.name
we.UserRole("admin")      // sets user.role
Event Inspection
evt.Fields()        // []wideevent.Field — snapshot of all key-value pairs
evt.FieldsMap()     // map[string]any — nested map with dot keys expanded
evt.HasError()      // bool — true if the latest outcome="failure" or latest status >= 500
evt.StatusCode()    // int — latest response status code, or 0 if not set
Nested Keys via Dots

Keys containing dots produce nested JSON:

we.Set("db.operation", "update")
we.Set("db.entity", "user")
// Output: {"db": {"operation": "update", "entity": "user"}}

Dev Mode

For local development, use DevStdoutEmitter() for human-readable, colorized output with unicode tree formatting:

r.Use(wideevent.Middleware(
    wideevent.WithEmitter(wideevent.DevStdoutEmitter()),
))

Output:

┌ GET /api/v1/users/abc-123                    200  12.4ms
│
│  request   method=GET  path=/api/v1/users/abc-123
│            host=localhost:8080  client_ip=127.0.0.1
│  response  status=200  body_size=1842  latency_ms=12.4
│  user      id=def-456  name=john  role=admin
│  db        operation=update  entity=user
│  cache     hit=false
│
└ outcome=success  duration=12.5ms

Colors are auto-detected based on terminal capability. Use WithColor(true/false) to override:

wideevent.DevStdoutEmitter(wideevent.WithColor(false)) // force no color

Use DevWriterEmitter for custom output targets:

wideevent.DevWriterEmitter(os.Stderr, wideevent.WithColor(true))

Combine with JSON for file logging using MultiEmitter:

wideevent.WithEmitter(wideevent.MultiEmitter(
    wideevent.DevStdoutEmitter(),          // pretty terminal output
    wideevent.JSONWriterEmitter(logFile),   // structured JSON to file
))
Path Categories & Muting

Group request paths into named categories and mute noisy ones:

wideevent.DevStdoutEmitter(
    wideevent.WithCategory("assets", "/src/assets", "/static"),
    wideevent.WithCategory("health", "/health", "/ready"),
    wideevent.WithCategory("api", "/api"),
    wideevent.WithMuteCategories("assets", "health"),
)

Muted categories produce no output. Non-muted categories show a label in the header:

┌ GET /api/v1/users/abc-123  [api]  200  12.4ms
│
│  ...
└ outcome=success  duration=12.5ms

Categories use prefix matching — WithCategory("assets", "/src/assets") matches /src/assets/logo.png, /src/assets/styles.css, etc.

Setup Options

r.Use(wideevent.Middleware(
    // Custom output target (default: JSON to stdout)
    wideevent.WithEmitter(wideevent.JSONWriterEmitter(os.Stderr)),

    // Tail sampling strategy (default: AlwaysSample)
    wideevent.WithSampler(wideevent.CompositeSampler(
        wideevent.AlwaysOnError(),
        wideevent.Rate(100),
    )),

    // Add fields before handler (e.g., trace ID)
    wideevent.WithPreEnricher(func(we *wideevent.Event, c *gin.Context) {
        we.Str("trace.id", c.GetHeader("X-Trace-ID"))
    }),

    // Add fields after handler (e.g., user context from auth)
    wideevent.WithPostEnricher(func(we *wideevent.Event, c *gin.Context) {
        if user := getUser(c); user != nil {
            we.UserID(user.ID).UserName(user.Name).UserRole(user.Role)
        }
    }),

    // Skip paths (e.g., health checks)
    wideevent.WithSkipPaths("/health", "/ready"),
))

Examples

Gin Request Logging
package main

import (
    "github.com/gin-gonic/gin"
    wideevent "github.com/mxcd/go-wide-event-logger"
)

func main() {
    r := gin.New()
    r.Use(wideevent.Middleware(
        wideevent.WithSkipPaths("/health"),
        wideevent.WithPostEnricher(func(we *wideevent.Event, c *gin.Context) {
            if u := getAuthUser(c); u != nil {
                we.UserID(u.ID).UserName(u.Username).UserRole(string(u.Role))
            }
        }),
    ))

    r.PUT("/api/v1/users/:id", func(c *gin.Context) {
        we := wideevent.FromGin(c)
        we.Str("endpoint", "update_user")

        id := c.Param("id")
        user, err := updateUser(c.Request.Context(), id)
        if err != nil {
            we.Failure(err)
            c.JSON(500, gin.H{"error": "internal"})
            return
        }
        we.Success()
        c.JSON(200, user)
    })

    r.Run(":8080")
}
Background Task Logging
func (s *Service) SyncUsers(ctx context.Context) {
    we := wideevent.Begin(s.emitter, "sync_users")
    defer we.Emit()

    we.Set("source", "cron").Set("trigger", "scheduled")

    users, err := s.fetchExternalUsers(ctx)
    if err != nil {
        we.Failure(err)
        return
    }
    we.Int("batch_size", len(users)).Success()
}
Repository Layer Enrichment
func (r *Repository) UpdateUser(ctx context.Context, id uuid.UUID, p *UpdateParams) (*ent.User, error) {
    we := wideevent.FromContext(ctx)
    if we != nil {
        we.Set("db.operation", "update").Set("db.entity", "user")
        we.Set("cache.hit", r.userCache.Has(id.String()))
    }

    user, err := r.client.User.UpdateOneID(id).SetUsername(p.Username).Save(ctx)
    if err != nil && we != nil {
        we.Set("db.error", err.Error())
    }
    return user, err
}
Custom Emitter

Implement the Emitter interface to send events anywhere — a database, an external service, etc. Use evt.Fields() for raw key-value pairs or evt.FieldsMap() for a pre-nested map[string]any.

type DBLogEmitter struct {
    db *ent.Client
}

func (e *DBLogEmitter) Emit(evt *wideevent.Event) {
    fields := evt.FieldsMap() // nested map[string]any, dot keys already expanded
    data, _ := json.Marshal(fields)
    e.db.Log.Create().
        SetLevel(fields["level"].(string)).
        SetData(data).
        SaveX(context.Background())
}
MultiEmitter

Use MultiEmitter to fan out events to multiple destinations simultaneously:

r.Use(wideevent.Middleware(
    wideevent.WithEmitter(wideevent.MultiEmitter(
        wideevent.JSONStdoutEmitter(),   // structured logs to stdout
        &DBLogEmitter{db: client},        // persist to database
    )),
))
Tail Sampling
// Production: always keep errors, sample 1-in-100 successes
sampler := wideevent.CompositeSampler(
    wideevent.AlwaysOnError(),
    wideevent.AlwaysOnStatus(429, 503),
    wideevent.Rate(100),
)

r.Use(wideevent.Middleware(
    wideevent.WithSampler(sampler),
))

JSON Output Format

Events are emitted as single JSON lines with:

  • Nested structure: dot-separated keys become nested objects
  • Level inference: HasError() or status >= 500 → "error", status >= 400 → "warn", else "info"
  • Durations: emitted as float64 milliseconds
  • Times: emitted as RFC3339Nano strings
  • Message: always "wide_event" for easy filtering
Automatic Fields (Middleware)

Request fields (set before handler):

  • request.method, request.path, request.query, request.host
  • request.proto, request.content_length, request.client_ip
  • request.user_agent, request.id, request.start_time

Response fields (set after handler):

  • response.status, response.body_size
  • response.latency, response.latency_ms
  • response.gin_errors, response.gin_error.N

Available Samplers

Sampler Description
AlwaysSample() Emit every event (default)
NeverSample() Emit nothing
AlwaysOnError() Emit if event has error
AlwaysOnStatus(codes...) Emit if status matches
Rate(n) Emit every Nth event
Probability(p) Emit with probability p (0.0-1.0)
CompositeSampler(s...) Emit if ANY sampler says yes (OR logic)

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Middleware

func Middleware(opts ...Option) gin.HandlerFunc

Middleware returns a Gin middleware that creates a wide event per request, enriches it with request/response metadata, and emits it after the handler completes.

func NewContext

func NewContext(ctx context.Context, evt *Event) context.Context

NewContext returns a new context with the event stored in it.

Types

type DevEmitterOption

type DevEmitterOption func(*devEmitterConfig)

DevEmitterOption configures a dev emitter.

func WithCategory

func WithCategory(name string, pathPrefixes ...string) DevEmitterOption

WithCategory groups request paths under a named category using prefix matching. Events whose request.path starts with any of the given prefixes are assigned to this category. Categories can be displayed in the header and selectively muted via WithMuteCategories.

func WithColor

func WithColor(enabled bool) DevEmitterOption

WithColor forces color output on or off, overriding TTY auto-detection.

func WithDurationThresholds

func WithDurationThresholds(warn, err time.Duration) DevEmitterOption

WithDurationThresholds configures the color scale for duration values in the footer. Durations below warn are green, between warn and err are yellow, at or above err are red. Default thresholds are 500ms (warn) and 2s (error).

func WithMuteCategories

func WithMuteCategories(names ...string) DevEmitterOption

WithMuteCategories silently drops events belonging to the named categories from dev output. Events in muted categories produce no output at all.

type Emitter

type Emitter interface {
	Emit(evt *Event)
}

Emitter defines the interface for outputting wide events.

func DevStdoutEmitter

func DevStdoutEmitter(opts ...DevEmitterOption) Emitter

DevStdoutEmitter returns an Emitter that writes human-readable, colorized wide events to stdout using unicode tree-line formatting. Colors are auto-detected based on whether stdout is a terminal.

func DevWriterEmitter

func DevWriterEmitter(w io.Writer, opts ...DevEmitterOption) Emitter

DevWriterEmitter returns a dev emitter that writes to the given writer. Colors are disabled by default; use WithColor(true) to force them on.

func JSONStdoutEmitter

func JSONStdoutEmitter() Emitter

JSONStdoutEmitter returns an Emitter that writes one JSON line per event to stdout.

func JSONWriterEmitter

func JSONWriterEmitter(w io.Writer) Emitter

JSONWriterEmitter returns an Emitter that writes one JSON line per event to the given writer.

func MultiEmitter

func MultiEmitter(emitters ...Emitter) Emitter

MultiEmitter fans out to multiple emitters. All emitters receive every event.

type EmitterFunc

type EmitterFunc func(evt *Event)

EmitterFunc is an adapter to allow the use of ordinary functions as Emitters.

func (EmitterFunc) Emit

func (f EmitterFunc) Emit(evt *Event)

type Event

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

Event represents a single wide event that accumulates structured fields throughout a unit of work and emits them as one rich log line.

func Begin

func Begin(emitter Emitter, name string) *Event

Begin creates a new standalone event for background tasks or cron jobs. The caller is responsible for calling Emit() when the work is done.

func FromContext

func FromContext(ctx context.Context) *Event

FromContext retrieves the event from a standard context. Returns nil if no event is stored.

func FromGin

func FromGin(c *gin.Context) *Event

FromGin retrieves the event from a Gin context. It first checks the Gin key store, then falls back to the request context. Returns nil if no event is found.

func (*Event) Bool

func (e *Event) Bool(key string, value bool) *Event

Bool adds a bool field.

func (*Event) Dur

func (e *Event) Dur(key string, value time.Duration) *Event

Dur adds a time.Duration field.

func (*Event) Emit

func (e *Event) Emit()

Emit finalizes the event and sends it to the emitter. Automatically sets the duration field. Safe to call multiple times; only the first call emits.

func (*Event) Err

func (e *Event) Err(key string, err error) *Event

Err adds an error field. If err is nil, this is a no-op.

func (*Event) Failure

func (e *Event) Failure(err error) *Event

Failure sets the outcome field to "failure" and records the error.

func (*Event) Fields

func (e *Event) Fields() []Field

Fields returns a snapshot copy of all fields.

func (*Event) FieldsMap

func (e *Event) FieldsMap() map[string]any

FieldsMap returns all fields as a nested map, expanding dot-separated keys. This is a convenience method for custom emitters that need structured data.

func (*Event) Float64

func (e *Event) Float64(key string, value float64) *Event

Float64 adds a float64 field.

func (*Event) HasError

func (e *Event) HasError() bool

HasError returns true if the event has an outcome of "failure" or a response status >= 500. Like the emitted output, it reads the latest value of each field.

func (*Event) Int

func (e *Event) Int(key string, value int) *Event

Int adds an int field.

func (*Event) Int64

func (e *Event) Int64(key string, value int64) *Event

Int64 adds an int64 field.

func (*Event) Interface

func (e *Event) Interface(key string, value any) *Event

Interface adds a field with any value (alias for Set).

func (*Event) Set

func (e *Event) Set(key string, value any) *Event

Set adds a field with any value. The key can contain dots for nested JSON output.

func (*Event) StatusCode

func (e *Event) StatusCode() int

StatusCode returns the latest response status code, or 0 if not set.

func (*Event) Str

func (e *Event) Str(key, value string) *Event

Str adds a string field.

func (*Event) Success

func (e *Event) Success() *Event

Success sets the outcome field to "success".

func (*Event) Time

func (e *Event) Time(key string, value time.Time) *Event

Time adds a time.Time field.

func (*Event) UUID

func (e *Event) UUID(key string, value fmt.Stringer) *Event

UUID adds a field from any fmt.Stringer (typically a UUID type).

func (*Event) UserID

func (e *Event) UserID(id fmt.Stringer) *Event

UserID sets the user.id field from any fmt.Stringer.

func (*Event) UserName

func (e *Event) UserName(name string) *Event

UserName sets the user.name field.

func (*Event) UserRole

func (e *Event) UserRole(role string) *Event

UserRole sets the user.role field.

type Field

type Field struct {
	Key   string
	Value any
}

Field represents a single key-value pair in a wide event.

type Option

type Option func(*middlewareConfig)

Option configures the wide event middleware.

func WithEmitter

func WithEmitter(emitter Emitter) Option

WithEmitter sets the emitter for the middleware. Default: JSONStdoutEmitter.

func WithPostEnricher

func WithPostEnricher(fn func(*Event, *gin.Context)) Option

WithPostEnricher sets a function called after the handler to add fields.

func WithPreEnricher

func WithPreEnricher(fn func(*Event, *gin.Context)) Option

WithPreEnricher sets a function called before the handler to add fields.

func WithSampler

func WithSampler(sampler Sampler) Option

WithSampler sets the tail sampler. Default: AlwaysSample.

func WithSkipPaths

func WithSkipPaths(paths ...string) Option

WithSkipPaths sets paths that should not generate wide events.

type Sampler

type Sampler interface {
	ShouldSample(evt *Event) bool
}

Sampler decides whether a completed event should be emitted.

func AlwaysOnError

func AlwaysOnError() Sampler

AlwaysOnError returns a sampler that emits if the event has an error.

func AlwaysOnStatus

func AlwaysOnStatus(codes ...int) Sampler

AlwaysOnStatus returns a sampler that emits if the response status matches any of the given codes.

func AlwaysSample

func AlwaysSample() Sampler

AlwaysSample returns a sampler that always emits.

func CompositeSampler

func CompositeSampler(samplers ...Sampler) Sampler

CompositeSampler returns a sampler that emits if ANY of the given samplers say yes (OR logic).

func NeverSample

func NeverSample() Sampler

NeverSample returns a sampler that never emits.

func Probability

func Probability(p float64) Sampler

Probability returns a sampler that emits with the given probability (0.0 to 1.0).

func Rate

func Rate(n int) Sampler

Rate returns a sampler that emits every Nth event.

type SamplerFunc

type SamplerFunc func(evt *Event) bool

SamplerFunc is an adapter to allow the use of ordinary functions as Samplers.

func (SamplerFunc) ShouldSample

func (f SamplerFunc) ShouldSample(evt *Event) bool

Jump to

Keyboard shortcuts

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