I Built happycontext: Wide Logging for Go with Router + Logger Adapters

/ 5 min read READ
Wide Logging for Go Services

One canonical event per request.

Built for Go services: gather context through middleware and handlers, then emit one structured request event through your existing logger.

5router integrations
3logger adapters
15interop combinations
Minimal blue diagram comparing fragmented logs to one wide event

One Event

Replace noisy middleware and handler logs with one request-scoped payload.

Full Context

Capture HTTP details, user metadata, and business data in one searchable record.

Router Scoped

Add fields as the request travels through net/http, gin, echo, or fiber.

Adapter Friendly

Keep your existing logger stack and switch only the sink integration.

Why I Built This

Most production logs are noisy but still missing context.

You can have thousands of lines and still not answer the basic question: what happened in this one request? That pain is exactly what posts like loggingsucks.com and evlog’s introduction to wide events call out.

I wanted a small Go library that keeps the good parts of request logging while reducing fragmentation.

happycontext does that by emitting one structured, request-scoped event at the end of each request lifecycle.

The core idea: one canonical event per request, with predictable fields and optional sampling.

What “Wide Logging” Means Here

Instead of emitting 6 to 20 lines through middleware and handlers, happycontext accumulates request context in-memory and writes one final event.

What are Wide Events?

Instead of scattering logs throughout your code:

Traditional logging

logger.info('Request started')
logger.Info("user authenticated", "user_id", userID)
logger.Info("fetching cart", "cart_id", cartID)
logger.Info("processing payment")
logger.Info("payment successful")
logger.Info("request completed")

Wide Event (Go)

// server/api/checkout.post.ts
func checkout(w http.ResponseWriter, r *http.Request) {
  hc.Add(r.Context(), "user_id", "u_8472", "feature", "checkout")
  hc.Add(r.Context(), "cart_id", "c_42", "items", 3, "total_cents", 9999)
  hc.Add(r.Context(), "payment_method", "card", "payment_status", "success")

  w.WriteHeader(http.StatusOK)
}

One log, all context, emitted once at request completion.

package main

import (
	"errors"
	"log/slog"
	"net/http"
	"os"

	hc "github.com/happytoolin/happycontext"
	slogadapter "github.com/happytoolin/happycontext/adapter/slog"
	stdhc "github.com/happytoolin/happycontext/integration/std"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
	sink := slogadapter.New(logger)

	mw := stdhc.Middleware(hc.Config{
		Sink:         sink,
		SamplingRate: 1.0,
		Message:      "request_completed",
	})

	mux := http.NewServeMux()
	mux.HandleFunc("GET /orders/{id}", func(w http.ResponseWriter, r *http.Request) {
		hc.Add(r.Context(), "user_id", "u_8472", "feature", "checkout")
		if r.URL.Query().Get("fail") == "1" {
			hc.Error(r.Context(), errors.New("checkout failed"))
			http.Error(w, "internal error", http.StatusInternalServerError)
			return
		}
		w.WriteHeader(http.StatusOK)
	})

	_ = http.ListenAndServe(":8080", mw(mux))
}

Typical final fields include:

  • http.method
  • http.path
  • http.route
  • http.status
  • duration_ms
  • your custom fields like user_id, tenant_id, feature, trace_id

This structure makes filtering and alerting much easier in Loki, Datadog, ELK, or any JSON log pipeline.

Wide Events, in Practice

A good wide event usually combines four layers:

  • Request context: method, path, route, request/trace IDs
  • User context: who made the call (user, tenant, plan)
  • Business context: cart/order/payment/domain fields
  • Outcome: status, duration, and error details (if any)

In Go terms, the pattern is simple: add context incrementally with hc.Add(...) as you learn more during the request, and let middleware emit the final event once.

hc.Add(ctx, "user_id", userID, "tenant_id", tenantID)
hc.Add(ctx, "order_id", orderID, "total_cents", totalCents)

if err != nil {
	hc.Error(ctx, err)
}

Prefer clear key names and grouped domain fields over generic blobs. That keeps queries readable and makes incidents faster to debug.

For a deeper walkthrough of the wide-event model, see: evlog.dev/core-concepts/wide-events.

Quick Start (net/http + slog)

package main

import (
	"errors"
	"log/slog"
	"net/http"
	"os"

	hc "github.com/happytoolin/happycontext"
	slogadapter "github.com/happytoolin/happycontext/adapter/slog"
	stdhc "github.com/happytoolin/happycontext/integration/std"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
	sink := slogadapter.New(logger)

	mw := stdhc.Middleware(hc.Config{
		Sink:         sink,
		SamplingRate: 1.0,
		Message:      "request_completed",
	})

	mux := http.NewServeMux()
	mux.HandleFunc("GET /orders/{id}", func(w http.ResponseWriter, r *http.Request) {
		hc.Add(r.Context(), "user_id", "u_8472", "feature", "checkout")

		if r.URL.Query().Get("fail") == "1" {
			hc.Error(r.Context(), errors.New("checkout failed"))
			http.Error(w, "internal error", http.StatusInternalServerError)
			return
		}

		w.WriteHeader(http.StatusOK)
	})

	_ = http.ListenAndServe(":8080", mw(mux))
}

Integrations and Adapters

happycontext intentionally separates router middleware from logger adapters.

Router + Adapter Matrix

Router integrations

  • integration/std (net/http)
  • integration/gin
  • integration/echo
  • integration/fiber (v2)
  • integration/fiberv3 (v3)

Logger adapters

  • adapter/slog
  • adapter/zap
  • adapter/zerolog

15 combinations without rewriting domain handlers.

Blue minimal router and logger adapter architecture for happycontext

Router integrations

  • integration/std (net/http)
  • integration/gin
  • integration/echo
  • integration/fiber (Fiber v2)
  • integration/fiberv3 (Fiber v3)

Logger adapters

  • adapter/slog
  • adapter/zap
  • adapter/zerolog

That gives you 15 router/logger combinations without changing your domain handlers.

01

Start

Middleware captures method, path, route, and a request timestamp.

02

Enrich

Handlers add user, tenant, feature, and domain payload as context arrives.

03

Finalize

Status, duration, and error fields are computed once the request is done.

04

Emit

The adapter writes one canonical event to slog, zap, or zerolog.

Sampling Without Losing Incidents

Wide events are richer, so sampling strategy matters.

The built-in path keeps failures and 5xx responses, then applies sampling to healthy traffic. You can also provide your own sampler chain.

mw := stdhc.Middleware(hc.Config{
	Sink: sink,
	Sampler: hc.ChainSampler(
		hc.RateSampler(0.05),
		hc.KeepErrors(),
		hc.KeepPathPrefix("/admin", "/checkout"),
		hc.KeepSlowerThan(500*time.Millisecond),
	),
})

Tail Sampling Rules

Always keep

  • 5xx status or panic
  • error field is present
  • /admin and /checkout paths
  • slow requests above threshold

Sample healthy traffic

  • Apply base rate to successful requests
  • Use per-level overrides when needed
  • Keep events query-friendly and compact
  • Tune rates after dashboard validation

Keep incidents at 100%, control cost on happy-path traffic.

Blue minimal tail-sampling decision path for happycontext

Benchmark Snapshot

From the benchmark report in the repo (Apple M4, February 10, 2026):

  • zerolog adapter write (small): ~155 ns/op, 0 allocs/op
  • zap adapter write (small): ~553 ns/op, 0 allocs/op
  • slog adapter write (small): ~742 ns/op, 7 allocs/op
  • Standard-library router middleware (net/http) with sink-noop: ~525 ns/op

Numbers will vary by machine and sink setup, but the trend is stable: adapters are lightweight and the middleware overhead is predictable.

Migration Plan for Existing Services

If your codebase already logs in many places, migrate in layers:

  1. Wrap one router with happycontext middleware first.
  2. Keep your existing logger and only add the matching sink adapter (slog, zap, or zerolog).
  3. Add only core fields at first: request_id, user_id, tenant_id, and feature.
  4. Keep SamplingRate=1.0 for the first rollout so you can validate full payload shape.
  5. Verify every request emits one canonical event with consistent key names.
  6. Add sampling rules only after dashboards and alerts are stable.
  7. Roll out integration-by-integration until all services follow the same schema.

Closing

happycontext is my attempt to make wide logging practical in Go without forcing a logger rewrite.

If you want to try it:

go get github.com/happytoolin/happycontext
go get github.com/happytoolin/happycontext/adapter/slog
go get github.com/happytoolin/happycontext/integration/std

Repo: github.com/happytoolin/happycontext