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, a Meter and an *slog.Logger and can be installed as a swappable global default or injected explicitly.
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 HTTPMiddleware, wrap an http.RoundTripper with RoundTripper, time a query with TimeQuery and record cache hits with NewCacheStats.
Example ¶
p := observability.NewProvider(observability.Options{ServiceName: "api"})
observability.SetGlobal(p)
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 := observability.HTTPMiddleware(p)(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)
- type Counter
- type Gauge
- type Histogram
- type HistogramSnapshot
- type Meter
- 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.
Types ¶
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 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. It also renders Prometheus text exposition output (see WriteProm in expose.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.