observability

package
v0.26.0 Latest Latest
Warning

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

Go to latest
Published: Jun 25, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package observability provides a vendor-neutral, dependency-free observability layer for the lagodev framework: tracing, metrics and trace-correlated logging.

The package is built around small interfaces (Tracer, Span, Meter, Counter, Gauge, Histogram) so that the stdlib default implementation can be swapped for a real backend (for example OpenTelemetry) through an adapter sub-module without touching call sites. A Provider bundles a Tracer and a Registry (the default Meter) so the integration layer wires from one object.

Everything here uses only the Go standard library. No OpenTelemetry or Prometheus client dependency is required:

  • Tracing records spans into an in-process ring buffer (for a future Telescope dashboard) and can emit them through log/slog. W3C traceparent propagation is implemented for net/http headers.
  • Metrics are kept in an in-memory registry that renders the Prometheus text exposition format directly and also publishes to expvar.
  • Logging is an slog.Handler wrapper that injects trace_id / span_id from the context into every record.

Opt-in wrappers

The middleware and helpers are returned for the caller to apply; this package never edits web, orm or other packages. Wrap an http.Handler with Middleware, wrap an http.RoundTripper with NewRoundTripper, and expose the registry with MetricsHandler. A Provider bundles a Tracer and a Registry so the whole stack wires from one object.

Example

p := observability.NewProvider(nil, nil) // default Tracer + Registry

mux := http.NewServeMux()
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
	_, span := p.Tracer().Start(r.Context(), "say-hello")
	defer span.End()
	w.Write([]byte("hi"))
})
mux.Handle("/metrics", p.MetricsHandler())

srv := p.Middleware()(mux)
http.ListenAndServe(":8080", srv)
Example

Example traces a parent/child span pair and records a counter and a histogram, then prints only the stable facts (names, the parent/child link, and metric aggregates) — never the random trace/span IDs.

package main

import (
	"context"
	"fmt"

	"github.com/devituz/lagodev/observability"
)

func main() {
	tr := observability.NewTracer()

	// Root span for an inbound request.
	ctx, parent := tr.Start(context.Background(), "http.request")
	parent.SetAttr("http.method", "GET")
	parent.SetAttr("http.route", "/users/{id}")

	// Child span for the DB query; deriving it from ctx links it to parent.
	_, child := tr.Start(ctx, "db.query")
	child.SetAttr("db.system", "postgres")
	child.SetStatus(observability.StatusOK, "")
	child.End()

	parent.SetStatus(observability.StatusOK, "")
	parent.End()

	pc := parent.Context()
	cc := child.Context()

	fmt.Println("parent name:", "http.request")
	fmt.Println("child name:", "db.query")
	fmt.Println("same trace:", cc.TraceID == pc.TraceID)
	fmt.Println("child is child of parent:", cc.ParentID == pc.SpanID)
	fmt.Println("parent is root:", pc.ParentID == "")

	// Metrics: a request counter and a latency histogram.
	reg := observability.NewRegistry()
	reqs := reg.Counter("http_requests_total", "route", "/users")
	reqs.Inc()
	reqs.Inc()
	reqs.Add(3)

	lat := reg.Histogram("http_request_seconds", nil, "route", "/users")
	lat.Observe(0.012)
	lat.Observe(0.4)
	lat.Observe(1.2)

	snap := lat.Snapshot()
	fmt.Printf("counter value: %.0f\n", reqs.Value())
	fmt.Println("histogram count:", snap.Count)
	fmt.Printf("histogram sum: %.3f\n", snap.Sum)

}
Output:
parent name: http.request
child name: db.query
same trace: true
child is child of parent: true
parent is root: true
counter value: 5
histogram count: 3
histogram sum: 1.612

Index

Examples

Constants

This section is empty.

Variables

View Source
var DefaultBuckets = []float64{
	0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10,
}

DefaultBuckets are latency-oriented bucket bounds in seconds.

Functions

func ContextWithSpanContext

func ContextWithSpanContext(ctx context.Context, sc SpanContext) context.Context

ContextWithSpanContext returns a copy of ctx carrying sc.

func FormatTraceparent

func FormatTraceparent(sc SpanContext) string

FormatTraceparent renders sc as a W3C version-00 traceparent value.

func InjectHTTP

func InjectHTTP(h http.Header, sc SpanContext)

InjectHTTP writes sc into h as a traceparent header.

func MetricsHandler added in v0.25.0

func MetricsHandler(reg *Registry) http.Handler

MetricsHandler returns an http.Handler that renders reg in the Prometheus text exposition format (version 0.0.4). It depends only on the standard library; no Prometheus client is required.

Counters and gauges are emitted as their respective TYPE; histograms are emitted as cumulative _bucket series (with a le="+Inf" bucket), plus _sum and _count. Series are written in a stable, deterministic order so the output is reproducible and diff-friendly.

mux.Handle("/metrics", observability.MetricsHandler(reg))

func Middleware added in v0.25.0

func Middleware(tracer Tracer, reg *Registry, opts ...HTTPOption) func(http.Handler) http.Handler

Middleware returns net/http middleware that wraps each request in a server span and records request metrics against reg.

For every request it:

  • extracts an inbound W3C traceparent and continues that trace (the new server span becomes a child of the remote span);
  • starts a span (default name "http.server") carrying http.method and http.target attributes;
  • increments an in-flight gauge for the duration of the request;
  • records a request counter and a latency histogram labeled by method and status code on completion;
  • sets the span status from the response status code (5xx -> error);
  • injects the active trace id into the response header (default "Trace-Id"), and the request context carries the span context so downstream handlers and loggers can read it.

Either tracer or reg may be nil to disable that half independently.

func NewRoundTripper added in v0.25.0

func NewRoundTripper(base http.RoundTripper, tracer Tracer, reg *Registry, opts ...HTTPOption) http.RoundTripper

NewRoundTripper wraps base with a RoundTripper that, for each outbound call, opens a client span (default name "http.client"), injects the active span as a W3C traceparent header into the outgoing request, and records an outbound request counter and latency histogram labeled by method and status code.

If base is nil, http.DefaultTransport is used. Either tracer or reg may be nil to disable that half independently. Install it on an http.Client:

client := &http.Client{
	Transport: observability.NewRoundTripper(nil, tracer, reg),
}

Types

type Counter

type Counter interface {
	Inc()
	Add(v float64)
	Value() float64
}

Counter is a monotonically increasing value.

type Gauge

type Gauge interface {
	Set(v float64)
	Add(v float64)
	Inc()
	Dec()
	Value() float64
}

Gauge is a value that can go up and down.

type HTTPOption added in v0.25.0

type HTTPOption func(*httpOptions)

HTTPOption customizes Middleware / NewRoundTripper behavior.

func WithBuckets added in v0.25.0

func WithBuckets(buckets []float64) HTTPOption

WithBuckets sets the latency histogram buckets (upper bounds in seconds). Passing nil keeps DefaultBuckets.

func WithMetricNames added in v0.25.0

func WithMetricNames(requestsTotal, requestDuration, inFlight string) HTTPOption

WithMetricNames overrides the metric names. An empty string for any argument keeps the current (default) name for that metric.

func WithSpanName added in v0.25.0

func WithSpanName(name string) HTTPOption

WithSpanName overrides the span name.

func WithTraceResponseHeader added in v0.25.0

func WithTraceResponseHeader(name string) HTTPOption

WithTraceResponseHeader sets the response header the server middleware uses to echo the active trace id back to the caller. Pass "" to disable it.

type Histogram

type Histogram interface {
	Observe(v float64)
	// Snapshot returns cumulative bucket counts (le bound -> count), total
	// count and sum.
	Snapshot() HistogramSnapshot
}

Histogram records a distribution of observations into fixed buckets.

type HistogramSnapshot

type HistogramSnapshot struct {
	Buckets []float64 // upper bounds
	Counts  []uint64  // cumulative counts aligned with Buckets
	Count   uint64    // total observations
	Sum     float64
}

HistogramSnapshot is an immutable view of a histogram's state.

type Meter

type Meter interface {
	Counter(name string, labels ...string) Counter
	Gauge(name string, labels ...string) Gauge
	// Histogram creates a histogram. buckets are upper bounds in seconds (or
	// any unit); pass nil for DefaultBuckets.
	Histogram(name string, buckets []float64, labels ...string) Histogram
}

Meter creates and stores instruments. Implementations must be safe for concurrent use. Instruments with the same name and label set are deduplicated so repeated lookups return the same object.

type Provider added in v0.25.0

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

Provider bundles a Tracer and a Registry so the whole observability stack can be wired with a single object. It is a thin convenience over the primitives: Provider holds no state of its own beyond the two roots, and every method delegates to the package-level helpers.

p := observability.NewProvider(observability.NewTracer(), observability.NewRegistry())
mux := http.NewServeMux()
mux.Handle("/metrics", p.MetricsHandler())
srv := p.Middleware()(mux)
client := &http.Client{Transport: p.RoundTripper(nil)}

func NewProvider added in v0.25.0

func NewProvider(tracer Tracer, reg *Registry) *Provider

NewProvider builds a Provider. If tracer is nil a default Tracer is created; if reg is nil a fresh Registry is created.

func (*Provider) MetricsHandler added in v0.25.0

func (p *Provider) MetricsHandler() http.Handler

MetricsHandler returns an http.Handler exposing this Provider's Registry in the Prometheus text exposition format.

func (*Provider) Middleware added in v0.25.0

func (p *Provider) Middleware(opts ...HTTPOption) func(http.Handler) http.Handler

Middleware returns the server middleware wired to this Provider's Tracer and Registry. See Middleware for the full behavior and available options.

func (*Provider) Registry added in v0.25.0

func (p *Provider) Registry() *Registry

Registry returns the bundled Registry (the default Meter).

func (*Provider) RoundTripper added in v0.25.0

func (p *Provider) RoundTripper(base http.RoundTripper, opts ...HTTPOption) http.RoundTripper

RoundTripper returns a client RoundTripper wired to this Provider's Tracer and Registry, wrapping base (nil uses http.DefaultTransport).

func (*Provider) Tracer added in v0.25.0

func (p *Provider) Tracer() Tracer

Tracer returns the bundled Tracer.

type Recorder

type Recorder interface {
	// Spans returns a snapshot of the most recent finished spans, oldest first.
	Spans() []SpanRecord
}

Recorder exposes the in-process span ring buffer. The default Tracer implements it; adapters may choose not to.

type Registry

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

Registry is the default in-memory Meter. MetricsHandler renders it in the Prometheus text exposition format (see metrics_handler.go).

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates an empty Registry.

func (*Registry) Counter

func (r *Registry) Counter(name string, labels ...string) Counter

func (*Registry) Gauge

func (r *Registry) Gauge(name string, labels ...string) Gauge

func (*Registry) Histogram

func (r *Registry) Histogram(name string, buckets []float64, labels ...string) Histogram

type Span

type Span interface {
	// SetAttr attaches a typed key/value pair to the span.
	SetAttr(key string, value any) Span
	// RecordError marks the span as failed and stores the error message.
	RecordError(err error) Span
	// SetStatus overrides the span status.
	SetStatus(status SpanStatus, desc string) Span
	// End finalizes the span and records its duration.
	End()
	// Context returns the span's W3C trace/span identifiers.
	Context() SpanContext
}

Span represents a single timed operation. Implementations must be safe for concurrent use.

type SpanContext

type SpanContext struct {
	TraceID  string // 16-byte trace id, lower-hex (32 chars)
	SpanID   string // 8-byte span id, lower-hex (16 chars)
	ParentID string // parent span id, empty for a root span
	Sampled  bool
}

SpanContext holds the W3C identifiers that identify a span.

func ExtractHTTP

func ExtractHTTP(h http.Header) (SpanContext, bool)

ExtractHTTP reads the traceparent header from h, returning the remote SpanContext if present and valid.

func ParseTraceparent

func ParseTraceparent(value string) (SpanContext, bool)

ParseTraceparent parses a W3C traceparent header value of the form

version "-" trace-id "-" parent-id "-" trace-flags

e.g. "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01". It returns the decoded SpanContext and ok=true on success.

func SpanContextFromContext

func SpanContextFromContext(ctx context.Context) (SpanContext, bool)

SpanContextFromContext extracts the active SpanContext, if any.

func (SpanContext) Valid

func (sc SpanContext) Valid() bool

Valid reports whether the context carries a usable trace and span id.

type SpanRecord

type SpanRecord struct {
	Name      string
	Context   SpanContext
	Start     time.Time
	End       time.Time
	Duration  time.Duration
	Status    SpanStatus
	StatusMsg string
	Err       string
	Attrs     map[string]any
}

SpanRecord is an immutable snapshot of a finished span. The ring buffer stores these for the Telescope dashboard to read.

type SpanStatus

type SpanStatus int

SpanStatus describes the terminal status of a span.

const (
	// StatusUnset means no explicit status was recorded.
	StatusUnset SpanStatus = iota
	// StatusOK means the operation completed successfully.
	StatusOK
	// StatusError means the operation failed.
	StatusError
)

func (SpanStatus) String

func (s SpanStatus) String() string

type Tracer

type Tracer interface {
	// Start begins a new span named name. The returned context carries the
	// new span so that downstream Start calls become children of it.
	Start(ctx context.Context, name string) (context.Context, Span)
}

Tracer starts spans. Implementations must be safe for concurrent use.

func NewTracer

func NewTracer(opts ...TracerOption) Tracer

NewTracer creates the default stdlib Tracer.

type TracerOption

type TracerOption func(*defaultTracer)

TracerOption configures a default Tracer.

func WithRingCapacity

func WithRingCapacity(n int) TracerOption

WithRingCapacity sets the number of finished spans retained in memory.

func WithSpanLogger

func WithSpanLogger(l *slog.Logger) TracerOption

WithSpanLogger makes the tracer emit each finished span via slog at the given logger.

Jump to

Keyboard shortcuts

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