hc

package module
v0.0.2 Latest Latest
Warning

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

Go to latest
Published: Feb 9, 2026 License: MIT Imports: 8 Imported by: 0

README

happycontext

happycontext banner

CI Release Go Reference Go Version

Most application logs are high-volume but low-context. happycontext helps Go services emit one structured, canonical event per request, so debugging and analysis start from a complete record instead of scattered lines.

happycontext log stream demo

Why happycontext?

  • Cleaner logs with one canonical event per request
  • Consistent fields across handlers, middleware, and frameworks
  • Built-in sampling for healthy traffic
  • Error and panic events are always preserved
  • Works with slog, zap, and zerolog
  • Integrates with net/http, gin, echo, fiber, and fiber v3

Design principle:

  • Prefer one context-rich request event over many fragmented log lines. happycontext before and after

Install

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

Install only the adapter and integration packages you use.

Quick Start (net/http + slog)

package main

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

	"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")
		hc.Add(r.Context(), "feature", "checkout")
		w.WriteHeader(http.StatusOK)
	})

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

Other quick starts:

  • net/http + zap and net/http + zerolog are in ## More Examples
  • gin, echo, fiber v2, and fiber v3 (with slog) are in ## More Examples
  • Runnable reference apps are in cmd/examples

Example output:

{
  "time": "2026-02-09T14:03:12.451Z",
  "level": "INFO",
  "msg": "request_completed",
  "duration_ms": 3,
  "feature": "checkout",
  "http.method": "GET",
  "http.path": "/orders/123",
  "http.route": "GET /orders/{id}",
  "http.status": 200,
  "user_id": "u_8472"
}

Configuration

hc.Config gives you the core controls:

  • Sink: destination logger adapter (required to emit events)
  • SamplingRate: 0 to 1 for healthy-request sampling
  • LevelSamplingRates: optional level-specific sampling overrides
  • Sampler: optional custom sampling function (full control)
  • Message: final log message (defaults to request_completed)

Notes:

  • Sampling is automatically bypassed for errors and server failures.
  • If no sink is configured, requests still run; logging is skipped.
  • Sampling behavior is consistent across all integrations (net/http, gin, echo, fiber, and fiber v3).
Sampling Customization

Per-level sampling:

mw := stdhc.Middleware(hc.Config{
	Sink:         sink,
	SamplingRate: 0.05, // default for healthy traffic
	LevelSamplingRates: map[hc.Level]float64{
		hc.LevelWarn:  1.0, // keep all warns
		hc.LevelDebug: 0.01,
	},
})

Custom sampler (route/user/latency rules):

mw := stdhc.Middleware(hc.Config{
	Sink: sink,
	Sampler: func(in hc.SampleInput) bool {
		// Always keep failures and slow requests.
		if in.HasError || in.StatusCode >= 500 {
			return true
		}
		if in.Duration > 2*time.Second {
			return true
		}
		// Keep checkout requests.
		if in.Path == "/api/checkout" {
			return true
		}
		// Keep enterprise requests based on event fields.
		fields := hc.EventFields(in.Event)
		tier, _ := fields["user_tier"].(string)
		return tier == "enterprise"
	},
})

Built-in sampler chain:

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

Sampler building blocks:

  • hc.ChainSampler(base, middlewares...): composes one final Sampler from middleware rules.
  • hc.AlwaysSampler(): base sampler that keeps every event.
  • hc.NeverSampler(): base sampler that drops every event.
  • hc.RateSampler(rate): base probabilistic sampler (0 drops all, 1 keeps all).
  • hc.KeepErrors(): middleware that keeps errored requests (HasError or 5xx).
  • hc.KeepPathPrefix("/checkout", "/admin"): middleware that keeps matching path prefixes.
  • hc.KeepSlowerThan(minDuration): middleware that keeps requests at/above a duration threshold.

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

More Examples

1. net/http + slog
package main

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

	"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})

	mux := http.NewServeMux()
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		hc.Add(r.Context(), "router", "net/http")
		w.WriteHeader(http.StatusOK)
	})

	_ = http.ListenAndServe(":8101", mw(mux))
}
2. gin + slog
package main

import (
	"log/slog"
	"os"

	"github.com/gin-gonic/gin"
	"github.com/happytoolin/happycontext"
	slogadapter "github.com/happytoolin/happycontext/adapter/slog"
	ginhc "github.com/happytoolin/happycontext/integration/gin"
)

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

	r := gin.New()
	r.Use(ginhc.Middleware(hc.Config{Sink: sink, SamplingRate: 1}))
	r.GET("/users/:id", func(c *gin.Context) {
		hc.Add(c.Request.Context(), "router", "gin")
		c.Status(200)
	})

	_ = r.Run(":8105")
}
3. fiber v2 + slog
package main

import (
	"log/slog"
	"os"

	"github.com/gofiber/fiber/v2"
	"github.com/happytoolin/happycontext"
	slogadapter "github.com/happytoolin/happycontext/adapter/slog"
	fiberhc "github.com/happytoolin/happycontext/integration/fiber"
)

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

	app := fiber.New()
	app.Use(fiberhc.Middleware(hc.Config{Sink: sink, SamplingRate: 1}))
	app.Get("/users/:id", func(c *fiber.Ctx) error {
		hc.Add(c.UserContext(), "router", "fiber-v2")
		return c.SendStatus(200)
	})

	_ = app.Listen(":8107")
}
4. fiber v3 + slog
package main

import (
	"log/slog"
	"os"

	"github.com/gofiber/fiber/v3"
	"github.com/happytoolin/happycontext"
	slogadapter "github.com/happytoolin/happycontext/adapter/slog"
	fiberv3hc "github.com/happytoolin/happycontext/integration/fiberv3"
)

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

	app := fiber.New()
	app.Use(fiberv3hc.Middleware(hc.Config{Sink: sink, SamplingRate: 1}))
	app.Get("/users/:id", func(c fiber.Ctx) error {
		hc.Add(c.Context(), "router", "fiber-v3")
		return c.SendStatus(200)
	})

	_ = app.Listen(":8108")
}
5. echo + slog
package main

import (
	"log/slog"
	"os"

	"github.com/happytoolin/happycontext"
	slogadapter "github.com/happytoolin/happycontext/adapter/slog"
	echohc "github.com/happytoolin/happycontext/integration/echo"
	"github.com/labstack/echo/v4"
)

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

	e := echo.New()
	e.Use(echohc.Middleware(hc.Config{Sink: sink, SamplingRate: 1}))
	e.GET("/users/:id", func(c echo.Context) error {
		hc.Add(c.Request().Context(), "router", "echo")
		return c.NoContent(200)
	})

	_ = e.Start(":8106")
}
6. net/http + zap
package main

import (
	"net/http"

	"github.com/happytoolin/happycontext"
	zapadapter "github.com/happytoolin/happycontext/adapter/zap"
	stdhc "github.com/happytoolin/happycontext/integration/std"
	"go.uber.org/zap"
)

func main() {
	logger := zap.NewExample()
	sink := zapadapter.New(logger)
	mw := stdhc.Middleware(hc.Config{Sink: sink, SamplingRate: 1})

	mux := http.NewServeMux()
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		hc.Add(r.Context(), "example", "adapter-zap")
		w.WriteHeader(http.StatusOK)
	})

	_ = http.ListenAndServe(":8102", mw(mux))
}
7. net/http + zerolog
package main

import (
	"net/http"
	"os"

	"github.com/happytoolin/happycontext"
	zerologadapter "github.com/happytoolin/happycontext/adapter/zerolog"
	stdhc "github.com/happytoolin/happycontext/integration/std"
	"github.com/rs/zerolog"
)

func main() {
	logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
	sink := zerologadapter.New(&logger)
	mw := stdhc.Middleware(hc.Config{Sink: sink, SamplingRate: 1})

	mux := http.NewServeMux()
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		hc.Add(r.Context(), "example", "adapter-zerolog")
		w.WriteHeader(http.StatusOK)
	})

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

Runnable commands are also available in cmd/examples:

cd cmd/examples
go run ./adapter-slog
go run ./adapter-zap
go run ./adapter-zerolog
go run ./router-std
go run ./router-gin
go run ./router-echo
go run ./router-fiber
go run ./router-fiberv3
go run ./sampling-inbuilt
go run ./sampling-custom

Release Process

  • CI: .github/workflows/ci.yml
  • Release automation: .github/workflows/release.yml
  • Go proxy sync: .github/workflows/go-proxy-sync.yml

References

License

MIT

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Add

func Add(ctx context.Context, key string, value any) bool

Add records one field on the event stored in ctx.

func AddMap

func AddMap(ctx context.Context, fields map[string]any) bool

AddMap merges all fields into the event stored in ctx.

func Commit

func Commit(ctx context.Context, sink Sink, level Level) bool

Commit writes the current event snapshot immediately via sink.

func Error

func Error(ctx context.Context, err error) bool

Error records err on the event stored in ctx.

func EventFields added in v0.0.2

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

EventFields returns a deep-copied immutable field snapshot for e.

func EventHasError added in v0.0.2

func EventHasError(e *Event) bool

EventHasError reports whether e has an attached error.

func EventStartTime added in v0.0.2

func EventStartTime(e *Event) time.Time

EventStartTime returns e's start time.

func SetLevel

func SetLevel(ctx context.Context, level Level) bool

SetLevel sets a requested level override for the event in ctx.

func SetRoute

func SetRoute(ctx context.Context, route string) bool

SetRoute sets a normalized route template on the event in ctx.

Types

type CapturedEvent

type CapturedEvent struct {
	Level   Level
	Message string
	Fields  map[string]any
}

CapturedEvent is one event captured by TestSink.

type Config

type Config struct {
	// Sink receives the finalized event.
	Sink Sink

	// SamplingRate controls random sampling for non-error requests in [0,1]. Default is 1.0.
	// 0.0 means no sampling, 1.0 means full sampling.
	SamplingRate float64

	// LevelSamplingRates optionally overrides SamplingRate by final log level.
	// Values are clamped into [0,1].
	LevelSamplingRates map[Level]float64

	// Sampler overrides built-in sampling when set.
	// Return true to keep and write the event.
	Sampler Sampler

	// Message is the final log message.
	Message string
}

Config controls request finalization behavior.

type Event

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

Event accumulates request-scoped structured fields.

func FromContext

func FromContext(ctx context.Context) *Event

FromContext returns the request event stored in ctx, or nil if absent.

func NewContext

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

NewContext attaches a new event to ctx and returns both.

type Level

type Level string

Level represents event severity.

const (
	// LevelDebug represents debug-level severity.
	LevelDebug Level = "DEBUG"
	// LevelInfo represents info-level severity.
	LevelInfo Level = "INFO"
	// LevelWarn represents warn-level severity.
	LevelWarn Level = "WARN"
	// LevelError represents error-level severity.
	LevelError Level = "ERROR"
)

func GetLevel

func GetLevel(ctx context.Context) (Level, bool)

GetLevel returns a previously requested level override from ctx.

type SampleInput added in v0.0.2

type SampleInput struct {
	Method     string
	Path       string
	StatusCode int
	Duration   time.Duration
	Level      Level
	HasError   bool
	Event      *Event
}

SampleInput contains finalized request data used for sampling decisions.

type Sampler added in v0.0.2

type Sampler func(SampleInput) bool

Sampler returns true when an event should be written.

func AlwaysSampler added in v0.0.2

func AlwaysSampler() Sampler

AlwaysSampler returns a sampler that keeps every event.

func ChainSampler added in v0.0.2

func ChainSampler(base Sampler, middlewares ...SamplerMiddleware) Sampler

ChainSampler composes base with middlewares.

Middlewares are applied in declaration order: ChainSampler(base, a, b) == a(b(base)).

Example
sampler := ChainSampler(
	RateSampler(0),
	KeepErrors(),
	KeepPathPrefix("/checkout"),
)

fmt.Println(sampler(SampleInput{Path: "/catalog", StatusCode: 200}))
fmt.Println(sampler(SampleInput{Path: "/checkout/start", StatusCode: 200}))
fmt.Println(sampler(SampleInput{Path: "/catalog", StatusCode: 503}))
Output:

false
true
true

func NeverSampler added in v0.0.2

func NeverSampler() Sampler

NeverSampler returns a sampler that drops every event.

func RateSampler added in v0.0.2

func RateSampler(rate float64) Sampler

RateSampler returns a probabilistic sampler using rate in [0,1].

Values <= 0 always drop. Values >= 1 always keep.

type SamplerMiddleware added in v0.0.2

type SamplerMiddleware func(next Sampler) Sampler

SamplerMiddleware wraps a sampler with additional decision logic.

func KeepErrors added in v0.0.2

func KeepErrors() SamplerMiddleware

KeepErrors returns middleware that keeps errored requests.

func KeepPathPrefix added in v0.0.2

func KeepPathPrefix(prefixes ...string) SamplerMiddleware

KeepPathPrefix returns middleware that keeps requests matching path prefixes.

func KeepSlowerThan added in v0.0.2

func KeepSlowerThan(minDuration time.Duration) SamplerMiddleware

KeepSlowerThan returns middleware that keeps requests at/above minDuration.

Negative durations are treated as zero.

type Sink

type Sink interface {
	Write(level Level, message string, fields map[string]any)
}

Sink receives finalized request events.

type TestSink

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

TestSink captures events in memory for tests.

func NewTestSink

func NewTestSink() *TestSink

NewTestSink returns an empty in-memory sink.

func (*TestSink) Events

func (t *TestSink) Events() []CapturedEvent

Events returns a copy of captured events.

func (*TestSink) Write

func (t *TestSink) Write(level Level, message string, fields map[string]any)

Write appends one captured event.

Directories

Path Synopsis
adapter
slog module
zap module
zerolog module
cmd
examples module
integration
echo module
fiber module
fiberv3 module
gin module
std module

Jump to

Keyboard shortcuts

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