slogx

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: May 3, 2026 License: MIT Imports: 19 Imported by: 0

README

░██████╗██╗░░░░░░█████╗░░██████╗░██╗░░██╗
██╔════╝██║░░░░░██╔══██╗██╔════╝░╚██╗██╔╝
╚█████╗░██║░░░░░██║░░██║██║░░██╗░░╚███╔╝░
░╚═══██╗██║░░░░░██║░░██║██║░░╚██╗░██╔██╗░
██████╔╝███████╗╚█████╔╝╚██████╔╝██╔╝╚██╗
╚═════╝░╚══════╝░╚════╝░░╚═════╝░╚═╝░░╚═╝

Go Reference Go Report Card q slogx is a small extension on top of Go's standard log/slog package that adds:

  • Hierarchical tracing via a single call at the top of any function (ctx = slogx.Context(ctx)). Every log record emitted through the default slog logger is automatically decorated with _traceId, _parentId, and _spanPath attributes.
  • Per-package log levels (plus a global level), configurable at startup and changeable live at runtime.
  • A pluggable Store (default: in-memory ring buffer) that retains records for later inspection.
  • An HTTP admin handler exposing a REST API and a single-page web UI that shows logs in a live, collapsible span tree.

You still write slog.InfoContext(ctx, ...) / slog.DebugContext(...) just like you always have. slogx only requires two touch points:

  1. slogx.Setup(...) once at program start.
  2. ctx = slogx.Context(ctx) on the first line of any function that accepts a context.Context.

Install

go get ella.to/slogx@v0.1.0

Requires Go 1.22+ (uses method-based mux patterns and generics in std).

Quick start

package main

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

    "ella.to/slogx"
)

func main() {
    slogx.Setup(
        slogx.GlobalLevel(slog.LevelInfo),
        slogx.PackageLevel("ella.to/app/api", slog.LevelDebug), // chatty only here
        slogx.PackageLevel("example.com/noisy", slogx.LevelOff), // silenced
    )

    mux := http.NewServeMux()
    mux.HandleFunc("GET /hello", hello)
    mux.Handle("/_slogx/", http.StripPrefix("/_slogx", slogx.HttpHandler()))

    http.ListenAndServe(":8080", slogx.Middleware(mux))
}

func hello(w http.ResponseWriter, r *http.Request) {
    ctx := slogx.Context(r.Context())
    greet(ctx, "world")
    w.Write([]byte("ok"))
}

func greet(ctx context.Context, who string) {
    ctx = slogx.Context(ctx)
    slog.InfoContext(ctx, "greeting", "who", who)
}

Then visit http://localhost:8080/_slogx/ for the debugging UI.

A full runnable example lives in example/.

Context & hierarchy

Every slogx.Context(ctx) call:

  1. Generates a new span id.
  2. On the first call for a trace, also generates a fresh trace id.
  3. Appends the span id to a slash-joined ancestor chain.

Every record emitted through the default slog logger is enriched with:

Attribute Description
_traceId Stable id shared across the entire trace.
_parentId Innermost span id (the scope that emitted the log).
_spanPath Slash-joined chain of ancestor span ids.
_goroutine true if the record was emitted from a span that crossed a go boundary (sticky for all descendants).

The UI infers the span tree entirely from _spanPath - no synthetic "span-start" marker log is ever emitted.

Goroutines

No new API is needed. The canonical rule still holds -- call ctx = slogx.Context(ctx) as the first line of the goroutine:

go func() {
    ctx := slogx.Context(ctx)
    slog.InfoContext(ctx, "worker started")
}()

slogx.Context records the goroutine id at span creation. If the parent span was born on a different goroutine, the new span is flagged as concurrent; every record emitted from it (or any further descendant span) carries _goroutine: true and shows a green "go" chip in the UI. The flag is sticky, so nested calls inside the goroutine are also clearly marked.

HTTP middleware

slogx.Middleware(next) establishes a root trace + span per inbound request. Trace id resolution order:

  1. ?log_trace_id=... query parameter (override),
  2. X-TRACE-ID HTTP header (propagation),
  3. newly generated if neither is present.

The resolved id is echoed back on the response X-TRACE-ID header so clients can correlate.

Admin API

slogx.HttpHandler() returns a http.Handler with:

  • GET / - single-page UI (embedded).
  • GET /traces?limit=100 - most-recent trace summaries.
  • GET /logs?traceId=... - filtered records for a trace.
  • GET /levels - { "global": "INFO", "packages": [{ "package": "...", "level": "..." }] }
  • PATCH /levels - {"global"?:"DEBUG","set"?:{"pkg":"DEBUG"},"unset"?:["pkg"]}

Mount it wherever you like (put it behind your own auth in production):

mux.Handle("/_slogx/", http.StripPrefix("/_slogx", slogx.HttpHandler()))

Levels

slogx filters by per-package log level on top of a global default:

  • GlobalLevel(level) sets the default minimum level for packages that have no explicit override.
  • PackageLevel(pattern, level) overrides the level for a package (longest matching prefix wins).
  • Valid levels are slog.LevelDebug, slog.LevelInfo, slog.LevelWarn, slog.LevelError, and slogx.LevelOff (silences a scope entirely).
  • Matching is prefix-based on Go import paths: ella.to/app also covers ella.to/app/api.

Common recipes:

// Only show warnings+ by default, but debug for one hot package:
slogx.Setup(
    slogx.GlobalLevel(slog.LevelWarn),
    slogx.PackageLevel("ella.to/app/api", slog.LevelDebug),
)

// Opt-in mode: everything off except a handful of packages:
slogx.Setup(
    slogx.GlobalLevel(slogx.LevelOff),
    slogx.PackageLevel("ella.to/app",   slog.LevelInfo),
    slogx.PackageLevel("ella.to/app/db", slog.LevelDebug),
)

// Silence noise while keeping a normal global default:
slogx.Setup(
    slogx.PackageLevel("example.com/noisy", slogx.LevelOff),
)

Levels can also be edited live from the UI's Levels tab, or via PATCH /levels. Changes take effect immediately for subsequent records.

Gotcha: The Go runtime reports package main as just "main", not the module path of the binary. If your app is ella.to/myapp with package main, address it as "main" in PackageLevel.

Setup options

Option Purpose Default
GlobalLevel(l) / Level(l) Default min level for packages with no override slog.LevelInfo
PackageLevel(pattern, l) Per-package (prefix) min level override (none)
Output(w) Where the stdout-style sink writes os.Stdout
WithFormat(FormatJSON|FormatText) Sink encoding FormatJSON
AddSource(bool) Include source info in sink output true
WithStore(s) Custom Store implementation in-memory ring
RingBufferSize(n) Capacity of default ring store 10_000

Custom store

Implement slogx.Store and pass it to Setup(slogx.WithStore(mine)):

type Store interface {
    Append(r Record)
    Query(q Query) []Record
    Traces(limit int) []TraceSummary
}

Notes / out of scope

  • No W3C traceparent propagation (only X-TRACE-ID).
  • No built-in persistence; plug in your own Store for SQLite, etc.
  • No authentication on the admin handler - mount it behind your own middleware.

License

MIT — see LICENSE for details.

Documentation

Overview

Package slogx extends the standard log/slog package with:

  • Hierarchical trace/span context propagation via Context(ctx).
  • Static and dynamic include/exclude filtering by Go package import path.
  • A pluggable Store for in-process log retention.
  • An HTTP admin + UI handler for live debugging.

Typical setup:

func main() {
    slogx.Setup(
        slogx.Includes("ella.to/example"),
        slogx.Excludes("example.com/noise"),
    )
    // ...
}

Inside any function that accepts a context, the caller establishes a new span by shadowing ctx:

func Sum(ctx context.Context, xs ...int) int {
    ctx = slogx.Context(ctx)
    slog.InfoContext(ctx, "summing", "n", len(xs))
    // ...
}

Index

Constants

View Source
const (
	AttrTraceID   = "_traceId"
	AttrParentID  = "_parentId"
	AttrSpanPath  = "_spanPath"
	AttrGoroutine = "_goroutine"
)

Attribute keys injected into every record.

View Source
const (
	DefaultRingBufferSize = 10_000
)

Default configuration values.

View Source
const HeaderTraceID = "X-TRACE-ID"

HeaderTraceID is the HTTP header the middleware reads/writes to carry a trace id across hops.

View Source
const LevelOff slog.Level = math.MaxInt32

LevelOff disables logging entirely for a scope (global or a package). It is just a very large slog.Level value so that no real record can meet its threshold.

View Source
const QueryTraceID = "log_trace_id"

QueryTraceID is the URL query parameter that overrides the header when present. Matches the user-facing spec.

Variables

This section is empty.

Functions

func Concurrent added in v0.1.0

func Concurrent(ctx context.Context) bool

Concurrent reports whether ctx belongs to a span that runs on a different goroutine than the span that created its parent context (i.e. it was reached by crossing a `go` statement). Once set, it stays set for all descendant spans.

func Context added in v0.1.0

func Context(ctx context.Context) context.Context

Context returns a derived context that starts a new span. On the first call (no trace-id in ctx) a new trace-id is also generated. The returned context carries:

  • trace id (stable for the whole trace),
  • current span id (this new span),
  • span path (slash-joined ancestor chain, including this span),
  • goroutine-concurrency flag (true if this span was created on a different goroutine than its parent, or inherited from such a span).

Use at the start of any function that accepts ctx:

ctx = slogx.Context(ctx)

func HttpHandler

func HttpHandler() http.Handler

HttpHandler returns the admin/UI handler for the currently active slogx. Setup must have been called first.

Endpoints (all JSON unless noted):

GET  /                       -> embedded index.html
GET  /traces?limit=100       -> [TraceSummary, ...]
GET  /logs?traceId=...&limit -> [Record, ...]
GET  /levels                 -> { "global": "INFO", "packages": [...] }
PATCH /levels                -> { "global"?: "DEBUG",
                                  "set"?: {"pkg":"DEBUG"},
                                  "unset"?: ["pkg"] }

func LevelName added in v0.1.0

func LevelName(l slog.Level) string

LevelName returns the canonical name for a slog.Level used in slogx config ("OFF", "DEBUG", "INFO", "WARN", "ERROR"). Offsets are preserved for levels that don't map to a canonical name.

func Middleware added in v0.1.0

func Middleware(next http.Handler) http.Handler

Middleware establishes a trace-id context and a root span for each inbound HTTP request so that logs emitted during request processing show up under a single trace in the UI.

Trace-id resolution order:

  1. ?log_trace_id=... (override)
  2. X-TRACE-ID header (propagation)
  3. generated

The resolved trace-id is also echoed on the response header.

func ParseLevel added in v0.1.0

func ParseLevel(s string) (slog.Level, error)

ParseLevel accepts a case-insensitive level name ("off", "debug", "info", "warn", "error") and returns the corresponding slog.Level (or LevelOff).

func SpanID added in v0.1.0

func SpanID(ctx context.Context) string

SpanID returns the current (innermost) span id from ctx, or "" if none.

func SpanPath added in v0.1.0

func SpanPath(ctx context.Context) string

SpanPath returns the slash-joined ancestor chain of span ids from ctx.

func TraceID added in v0.1.0

func TraceID(ctx context.Context) string

TraceID returns the current trace id from ctx, or "" if none.

func WithTraceID added in v0.1.0

func WithTraceID(ctx context.Context, traceID string) context.Context

WithTraceID seeds a trace id into ctx without starting a new span. It is used by Middleware to honor inbound X-TRACE-ID headers before the root Context call.

Types

type Format added in v0.1.0

type Format int

Format selects the serialization used for the stdout/file sink.

const (
	FormatJSON Format = iota
	FormatText
)

type Handler added in v0.1.0

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

Handler is slogx's custom slog.Handler. It:

  • enriches every record with _traceId, _parentId, _spanPath from ctx,
  • filters records by a global + per-package level set,
  • fans out to a user-visible slog sink (JSON/text to an io.Writer) and a pluggable Store for the UI.

func Setup added in v0.1.0

func Setup(opts ...Option) *Handler

Setup installs slogx as the default slog logger and returns the installed Handler. It is safe to call Setup more than once (the previous handler is replaced).

func (*Handler) Enabled added in v0.1.0

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

Enabled implements slog.Handler. Uses the level-set floor so slog can skip record construction entirely when no rule could possibly allow the level.

func (*Handler) Handle added in v0.1.0

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

Handle implements slog.Handler.

func (*Handler) Levels added in v0.1.0

func (h *Handler) Levels() *levelSet

Levels returns the handler's live level set. Exposed for the admin HTTP API.

func (*Handler) Store added in v0.1.0

func (h *Handler) Store() Store

Store returns the handler's live Store. Exposed for the admin HTTP API.

func (*Handler) WithAttrs added in v0.1.0

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

WithAttrs implements slog.Handler.

func (*Handler) WithGroup added in v0.1.0

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

WithGroup implements slog.Handler.

type Option added in v0.1.0

type Option func(*config)

Option configures Setup.

func AddSource added in v0.1.0

func AddSource(v bool) Option

AddSource controls whether the underlying slog sink includes source info.

func GlobalLevel added in v0.1.0

func GlobalLevel(l slog.Level) Option

GlobalLevel sets the minimum level for records from packages that have no explicit override. Use LevelOff to turn logging off by default (handy when you want to opt-in specific packages with PackageLevel).

func Level added in v0.1.0

func Level(l slog.Level) Option

Level is an alias for GlobalLevel. Kept for ergonomic parity with slog.

func Output added in v0.1.0

func Output(w io.Writer) Option

Output sets the destination for the stdout-style sink. Default: os.Stdout. Pass io.Discard to disable the sink entirely.

func PackageLevel added in v0.1.0

func PackageLevel(pattern string, l slog.Level) Option

PackageLevel sets a per-package (prefix) minimum level that overrides the global level. Longest matching prefix wins. Pass LevelOff to silence a package entirely.

slogx.PackageLevel("ella.to/noisy", slogx.LevelOff)
slogx.PackageLevel("ella.to/app/api", slog.LevelDebug)

func RingBufferSize added in v0.1.0

func RingBufferSize(n int) Option

RingBufferSize sets the capacity of the default ring buffer store. Ignored if WithStore was also provided.

func WithFormat added in v0.1.0

func WithFormat(f Format) Option

WithFormat selects JSON or text output for the stdout-style sink.

func WithStore added in v0.1.0

func WithStore(s Store) Option

WithStore installs a custom Store. Default is an in-memory ring buffer.

type Query added in v0.1.0

type Query struct {
	TraceID string
	Since   time.Time
	Limit   int
}

Query restricts which records Query returns.

type Record added in v0.1.0

type Record struct {
	Time       time.Time      `json:"time"`
	Level      slog.Level     `json:"level"`
	Message    string         `json:"message"`
	Source     string         `json:"source,omitempty"`
	Package    string         `json:"package,omitempty"`
	TraceID    string         `json:"traceId,omitempty"`
	ParentID   string         `json:"parentId,omitempty"`
	SpanPath   string         `json:"spanPath,omitempty"`
	Concurrent bool           `json:"concurrent,omitempty"`
	Attrs      map[string]any `json:"attrs,omitempty"`
}

Record is the serialized form of a slog.Record as retained by a Store.

type Store added in v0.1.0

type Store interface {
	Append(r Record)
	Query(q Query) []Record
	Traces(limit int) []TraceSummary
}

Store retains log records for later inspection.

type TraceSummary added in v0.1.0

type TraceSummary struct {
	TraceID   string     `json:"traceId"`
	FirstTime time.Time  `json:"firstTime"`
	LastTime  time.Time  `json:"lastTime"`
	Count     int        `json:"count"`
	RootMsg   string     `json:"rootMessage,omitempty"`
	MaxLevel  slog.Level `json:"maxLevel"`
}

TraceSummary is a lightweight aggregate used by the UI index.

Directories

Path Synopsis
Example HTTP service wired with slogx.
Example HTTP service wired with slogx.

Jump to

Keyboard shortcuts

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