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 ¶
- Variables
- func ContextWithSpanContext(ctx context.Context, sc SpanContext) context.Context
- func FormatTraceparent(sc SpanContext) string
- func InjectHTTP(h http.Header, sc SpanContext)
- func MetricsHandler(reg *Registry) http.Handler
- func Middleware(tracer Tracer, reg *Registry, opts ...HTTPOption) func(http.Handler) http.Handler
- func NewRoundTripper(base http.RoundTripper, tracer Tracer, reg *Registry, opts ...HTTPOption) http.RoundTripper
- type Counter
- type Gauge
- type HTTPOption
- type Histogram
- type HistogramSnapshot
- type Meter
- type Provider
- type Recorder
- type Registry
- type Span
- type SpanContext
- type SpanRecord
- type SpanStatus
- type Tracer
- type TracerOption
Examples ¶
Constants ¶
This section is empty.
Variables ¶
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
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
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 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
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
MetricsHandler returns an http.Handler exposing this Provider's Registry in the Prometheus text exposition format.
func (*Provider) Middleware ¶ added in v0.25.0
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
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).
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).
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.