observability

package
v0.24.0 Latest Latest
Warning

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

Go to latest
Published: Jun 24, 2026 License: MIT Imports: 11 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, 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

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.

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 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).

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