observability

package
v11.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2026 License: AGPL-3.0 Imports: 26 Imported by: 0

Documentation

Overview

Package observability provides unified configuration and initialization for the four observability pillars: logging, metrics, tracing, and profiling.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AcknowledgeError

func AcknowledgeError(err error, logger logging.Logger, span tracing.Span, descriptionFmt string, descriptionArgs ...any)

AcknowledgeError standardizes our error handling by logging and tracing consistently.

A nil error is nothing to acknowledge, and returns without emitting — matching PrepareError and PrepareAndLogError, which already return nil untouched.

An empty description does not: this used to drop the error entirely, which made the one argument that is only ever decoration decide whether the error was reported at all.

func ObserveValues

func ObserveValues(values map[string]any, span tracing.Span, logger logging.Logger) logging.Logger

func PrepareAndLogError

func PrepareAndLogError(err error, logger logging.Logger, span tracing.Span, descriptionFmt string, descriptionArgs ...any) error

PrepareAndLogError standardizes our error handling by logging, tracing, and formatting an error consistently.

func PrepareAndLogGRPCStatus

func PrepareAndLogGRPCStatus(err error, logger logging.Logger, span tracing.Span, code codes.Code, descriptionFmt string, descriptionArgs ...any) error

PrepareAndLogGRPCStatus standardizes our error handling by logging, tracing, and formatting an error consistently.

func PrepareError

func PrepareError(err error, span tracing.Span, descriptionFmt string, descriptionArgs ...any) error

PrepareError standardizes our error handling by logging, tracing, and formatting an error consistently.

func RegisterO11yConfigs

func RegisterO11yConfigs(i do.Injector)

RegisterO11yConfigs registers sub-configs extracted from *Config with the injector. This extracts sub-configs from the parent *Config and registers them with the injector. Prerequisite: *Config must be registered in the injector before calling this.

Types

type BeginOption

type BeginOption func(Operation)

BeginOption seeds the Operation an Observer returns, before the caller's own code runs against it.

It exists because the overwhelmingly common shape of an instrumented method is to begin an operation and immediately describe it:

ctx, op := w.o11y.Begin(ctx)
defer op.End()

op.SetValues(map[string]any{
	requestIDKey: req.ID,
	statusKey:    string(req.Status),
})

which separates the values from the operation they describe by a statement that must sit between them. Passing them to Begin keeps the description attached to what it describes:

ctx, op := w.o11y.Begin(ctx, observability.WithValues(map[string]any{
	requestIDKey: req.ID,
	statusKey:    string(req.Status),
}))
defer op.End()

Options run in the order given, against the Operation and not the span directly, so a value seeded here lands on exactly the pillars the equivalent Operation method would have put it on — and a RecordingObserver sees it as an ordinary observation rather than as a special case.

BeginCustom takes trace.SpanStartOption instead: a Go function may have only one variadic parameter, and for an explicitly named span the span options are the ones worth having. Seed such an operation by calling the Operation methods directly.

func WithLogValue

func WithLogValue(key string, value any) BeginOption

WithLogValue records one value to the logger only, as LogOnly does.

func WithSpanValue

func WithSpanValue(key string, value any) BeginOption

WithSpanValue records one value to the span only, as SpanOnly does. Use it for values worth keeping on a trace but too noisy to repeat on every log line the operation emits.

func WithValue

func WithValue(key string, value any) BeginOption

WithValue records one value to both the span and the logger, as Set does.

func WithValues

func WithValues(values map[string]any) BeginOption

WithValues records values to both the span and the logger, as SetValues does.

A nil or empty map is a no-op, so a caller may pass a map it built conditionally without guarding the call.

type Config

type Config struct {
	Profiling profilingcfg.Config `envPrefix:"PROFILING_" json:"profiling,omitzero"`
	Logging   loggingcfg.Config   `envPrefix:"LOGGING_"   json:"logging,omitzero"`
	Metrics   metricscfg.Config   `envPrefix:"METRICS_"   json:"metrics,omitzero"`
	Tracing   tracingcfg.Config   `envPrefix:"TRACING_"   json:"tracing,omitzero"`
	// contains filtered or unexported fields
}

Config contains settings about how we report our metrics.

func (*Config) NewPillars

func (cfg *Config) NewPillars(ctx context.Context) (*Pillars, error)

NewPillars creates and returns all four observability pillars.

The whole config is validated before any of them is built, rather than leaving each constructor to validate its own on the way past. The pillars have side effects — a logger that opens an exporter connection, a profiler that starts an agent — and failing on the third one leaves the first two running with nothing to shut them down.

func (*Config) ValidateWithContext

func (cfg *Config) ValidateWithContext(ctx context.Context) error

ValidateWithContext validates a Config struct.

The sub-configs are struct values whose ValidateWithContext methods have pointer receivers. ozzo dereferences each field pointer to a value before checking for the ValidatableWithContext interface, so a bare validation.Field(&cfg.Logging) never invokes the sub-config's validation. Invoke each one explicitly instead.

type Matcher

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

Matcher describes a predicate over a single Observation, used by the ordered and per-operation assertion helpers.

func ObservedKey

func ObservedKey(key string) Matcher

ObservedKey matches any observation with the given key, whatever its value or pillar. Use it when a test knows a key was observed but not the value.

func ObservedKeyFunc

func ObservedKeyFunc(key string, pred func(value any) bool) Matcher

ObservedKeyFunc matches an observation with the given key whose value satisfies the predicate. Use it to assert a key was observed with a value of some shape without pinning the exact value.

func ObservedKeyValue

func ObservedKeyValue(key string, value any) Matcher

ObservedKeyValue matches an observation with the given key and a value that is deeply equal to the given value.

func ObservedValue

func ObservedValue(value any) Matcher

ObservedValue matches an observation with a value deeply equal to the given value, under any key.

func (Matcher) OnLog

func (m Matcher) OnLog() Matcher

OnLog refines a matcher to require the observation reached the logger pillar (Set or LogOnly).

func (Matcher) OnSpan

func (m Matcher) OnSpan() Matcher

OnSpan refines a matcher to require the observation reached the span pillar (Set or SpanOnly).

type Observation

type Observation struct {
	Value  any
	Key    string
	Seq    int
	Pillar Pillar
}

Observation is a single recorded attachment, in the order it occurred. Seq is a monotonic counter shared across every Operation of the owning observer, so the relative order of observations from different operations is recoverable.

type Observer

type Observer interface {
	Begin(ctx context.Context, opts ...BeginOption) (context.Context, Operation)
	BeginCustom(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, Operation)
	Logger() logging.Logger
	Tracer() tracing.Tracer
}

Observer bundles a named logger and tracer for a single component, so that a component holds one observability field instead of a logger/tracer pair. Each traced operation begins via Begin, which returns an Operation that records selected values to the active span and a span-linked logger simultaneously.

It is an interface so that unit tests can substitute a recording implementation (see NewRecordingObserver) and assert which fields a unit observed.

func NewObserver

func NewObserver(name string, logger logging.Logger, tracerProvider tracing.Provider) Observer

NewObserver builds the production Observer from the standard DI dependencies. The name is applied to both the logger and the tracer, mirroring the prior logging.NewNamedLogger / tracing.NewNamedTracer pair.

Every constructor in this module reaches observability through here, which is what makes a span's instrumentation scope predictable: it is always the component's own name, never whatever the caller happened to name a tracer it built itself. A constructor that accepted a ready-made tracing.Tracer would scope its spans by accident, so none of them do. A nil tracerProvider is safe: NewNamedTracer substitutes a noop provider, so an unconfigured component traces nowhere rather than panicking.

func NewObserverForTest

func NewObserverForTest(name string) Observer

NewObserverForTest builds an Observer backed by a noop logger and tracer, for code that just needs a functioning Observer in tests. To assert which values a unit attaches, use NewRecordingObserver instead.

func NewObserverWithValues

func NewObserverWithValues(
	name string,
	logger logging.Logger,
	tracerProvider tracing.Provider,
	values map[string]any,
) Observer

NewObserverWithValues is NewObserver for a component whose every operation describes the same thing: a listener bound to one channel, a worker bound to one queue. The values land on the component's logger and are seeded onto every Operation Begin returns, so a field that is constant for the component's lifetime is stated once at construction instead of at every call site — and, more to the point, cannot be stated at some of them and forgotten at the rest.

They are seeded before the caller's own BeginOptions, so an operation may still override one.

It is a constructor rather than a method on Observer because Observer is an exported interface: a method would break every implementation outside this module, and the values are known where the observer is built anyway.

type Operation

type Operation interface {
	Set(key string, value any) Operation
	SetValues(values map[string]any) Operation
	SpanOnly(key string, value any) Operation
	LogOnly(key string, value any) Operation
	Logger() logging.Logger
	Span() tracing.Span
	Time(ctx context.Context, c clock.Clock, hist metrics.Float64Histogram, opts ...metric.RecordOption) func()
	Error(err error, descriptionFmt string, descriptionArgs ...any) error
	Acknowledge(err error, descriptionFmt string, descriptionArgs ...any)
	GRPCStatus(err error, code codes.Code, descriptionFmt string, descriptionArgs ...any) error
	End()
}

Operation is the per-call observability bag returned by Observer.Begin. A value recorded via Set lands on both the active span and the running logger, so a value selected once is available to either pillar later. SpanOnly and LogOnly are escape hatches for the occasions where a value belongs to just one.

It is an interface so that a recording Observer can hand back an Operation a test can read values off of (see RecordingOperation).

type Pillar

type Pillar uint8

Pillar identifies where an observation landed.

const (
	// PillarBoth marks a value recorded via Set (span and logger).
	PillarBoth Pillar = iota
	// PillarSpan marks a value recorded via SpanOnly.
	PillarSpan
	// PillarLog marks a value recorded via LogOnly.
	PillarLog
)

type Pillars

type Pillars struct {
	Logger          logging.Logger
	TracerProvider  tracing.Provider
	MetricsProvider metrics.Provider
	Profiler        profiling.Provider
}

Pillars holds the four observability pillars: logging, tracing, metrics, and profiling.

func InvokePillars

func InvokePillars(i do.Injector) (*Pillars, error)

InvokePillars assembles whatever observability the injector has been given, requiring none of it.

A registered *Pillars wins outright; otherwise each pillar is looked up on its own and left nil when absent. Nil is the point — every constructor in this module resolves an absent dependency to its noop, so a service that registers no observability at all wires up and runs silently instead of panicking on a do.MustInvoke for a provider it never wanted.

A service that *is* registered but fails to build is a different matter, and is returned as an error rather than quietly treated as absent: a metrics provider whose exporter cannot reach its collector should surface, not degrade to a noop that looks configured.

func (*Pillars) Deps

func (p *Pillars) Deps() (logger logging.Logger, tracerProvider tracing.Provider, metricsProvider metrics.Provider)

Deps returns the three pillars a component can be instrumented with, in the order every config subpackage's WithPillars option assigns them.

It is nil-safe, which is the point: a caller that has no Pillars passes nil and gets three nil dependencies, each of which every constructor already resolves to its noop. That keeps the WithPillars option in ~25 config subpackages a single assignment instead of a nil check repeated per package.

The profiler is deliberately absent. Nothing is instrumented with it — it is a process-wide agent that Pillars owns for shutdown, not a dependency a component takes.

func (*Pillars) Shutdown

func (p *Pillars) Shutdown(ctx context.Context) error

Shutdown gracefully stops the observability pillars, flushing any buffered telemetry so records are not dropped on exit. It is safe to call on a partially populated Pillars.

type RecordingObserver

type RecordingObserver struct {
	Operations []*RecordingOperation
	// contains filtered or unexported fields
}

RecordingObserver is an Observer implementation for unit tests. It captures, in order, every value attached to the Operations it hands out, so a test can assert which fields a unit observed, in what order, and on which pillar. It performs no real logging or tracing.

func NewRecordingObserver

func NewRecordingObserver() *RecordingObserver

NewRecordingObserver builds a RecordingObserver for use in unit tests.

func NewRecordingObserverWithValues

func NewRecordingObserverWithValues(values map[string]any) *RecordingObserver

NewRecordingObserverWithValues is NewRecordingObserver for a unit whose real observer came from NewObserverWithValues. Pass the same values that constructor was given: they are seeded onto every Operation this observer hands out, so a test that substitutes the double still sees the fields the component states once at construction rather than at each call site.

Without it, moving a value from the call sites to the constructor would delete the assertion that the component observes it at all — the test would pass a key the double never records.

The values are recorded on the span pillar, mirroring observer.seed. In production their logger half comes from the named logger built alongside them, which this double's noop Logger does not model; assert them through ObservedOperationWith* (which unions both pillars) rather than LogValues.

func (*RecordingObserver) Begin

Begin returns a RecordingOperation seeded with opts, and leaves the context untouched. Seeded values are recorded as ordinary observations, so a test asserting on them cannot tell whether the unit passed them to Begin or set them afterwards — which is what makes migrating a call site to the option form invisible to its test.

func (*RecordingObserver) BeginCustom

BeginCustom returns a RecordingOperation and leaves the context untouched.

func (*RecordingObserver) Logger

func (o *RecordingObserver) Logger() logging.Logger

Logger returns a noop logger.

func (*RecordingObserver) ObservedInOrder

func (o *RecordingObserver) ObservedInOrder(t TestingT, matchers ...Matcher)

ObservedInOrder asserts that the given matchers occur, in order, somewhere in the global observation stream. Gaps between matches are allowed, so it reads as "this happened, then later that happened" across any operations.

func (*RecordingObserver) ObservedOperationWithData

func (o *RecordingObserver) ObservedOperationWithData(t TestingT, data map[string]any) *RecordingOperation

ObservedOperationWithData asserts that some recorded operation observed all of the given key/value pairs and that that operation ended, returning the matched operation. It does not care whether the operation also recorded an error, so it holds equally on success and failure paths; assert on the returned op's Errors to verify an error path specifically.

func (*RecordingObserver) ObservedOperationWithKeys

func (o *RecordingObserver) ObservedOperationWithKeys(t TestingT, keys ...string) *RecordingOperation

ObservedOperationWithKeys asserts that some recorded operation observed all of the given keys (on either pillar) and that that operation ended, returning the matched operation so callers can make further assertions (e.g. on its Errors).

func (*RecordingObserver) ObservedOperationWithValues

func (o *RecordingObserver) ObservedOperationWithValues(t TestingT, values ...any) *RecordingOperation

ObservedOperationWithValues asserts that some recorded operation observed all of the given values (under any key) and that that operation ended, returning the matched operation.

func (*RecordingObserver) Stream

func (o *RecordingObserver) Stream() []Observation

Stream returns every observation across all operations, in global order.

func (*RecordingObserver) Tracer

func (o *RecordingObserver) Tracer() tracing.Tracer

Tracer returns a noop tracer.

type RecordingOperation

type RecordingOperation struct {
	Observations []Observation
	Values       map[string]any
	SpanValues   map[string]any
	LogValues    map[string]any
	// Errors holds every error passed to Error, Acknowledge, or GRPCStatus.
	Errors []error
	// Ended reports whether End was called.
	Ended bool
	// contains filtered or unexported fields
}

RecordingOperation is the Operation handed out by RecordingObserver. Its exported maps record which keys reached which pillar (Values = Set, SpanValues = Set + SpanOnly, LogValues = Set + LogOnly), and Observations records the same attachments in order for precise, order-sensitive assertions.

func (*RecordingOperation) Acknowledge

func (op *RecordingOperation) Acknowledge(err error, _ string, _ ...any)

Acknowledge records err.

func (*RecordingOperation) End

func (op *RecordingOperation) End()

End marks the operation ended.

func (*RecordingOperation) Error

func (op *RecordingOperation) Error(err error, descriptionFmt string, descriptionArgs ...any) error

Error records err and returns it wrapped, matching the production Operation's returned-error shape (without logging or tracing).

func (*RecordingOperation) GRPCStatus

func (op *RecordingOperation) GRPCStatus(err error, code codes.Code, descriptionFmt string, descriptionArgs ...any) error

GRPCStatus records err and returns it as a gRPC status error, matching the production Operation's returned-error shape.

func (*RecordingOperation) LogOnly

func (op *RecordingOperation) LogOnly(key string, value any) Operation

LogOnly records a value to the logger pillar only.

func (*RecordingOperation) Logger

func (op *RecordingOperation) Logger() logging.Logger

Logger returns a noop logger.

func (*RecordingOperation) Observed

func (op *RecordingOperation) Observed(t TestingT, matchers ...Matcher)

Observed asserts that each matcher matches some observation in this operation, regardless of order.

func (*RecordingOperation) ObservedInOrder

func (op *RecordingOperation) ObservedInOrder(t TestingT, matchers ...Matcher)

ObservedInOrder asserts that the given matchers occur, in order, within this operation. Gaps between matches are allowed.

func (*RecordingOperation) Set

func (op *RecordingOperation) Set(key string, value any) Operation

Set records a value to both pillars.

func (*RecordingOperation) SetValues

func (op *RecordingOperation) SetValues(values map[string]any) Operation

SetValues records every value via Set.

func (*RecordingOperation) Span

func (op *RecordingOperation) Span() tracing.Span

Span returns nil; recording operations have no real span.

func (*RecordingOperation) SpanOnly

func (op *RecordingOperation) SpanOnly(key string, value any) Operation

SpanOnly records a value to the span pillar only.

func (*RecordingOperation) Time

func (op *RecordingOperation) Time(ctx context.Context, c clock.Clock, hist metrics.Float64Histogram, opts ...metric.RecordOption) func()

Time measures through the same code the production Operation does, so a test holding a RecordingOperation still records into whatever histogram it was given — which is how a test asserts on a latency it controls through an injected clock.

type TestingT

type TestingT interface {
	Helper()
	Fatalf(format string, args ...any)
}

TestingT is the minimal subset of *testing.T the assertion helpers need. It lets RecordingObserver offer ergonomic assertions without importing the testing package into production builds; *testing.T satisfies it structurally.

Directories

Path Synopsis
Package keys is the module's attribute-name vocabulary: the string constants every package uses when it puts a value on a span or a log line.
Package keys is the module's attribute-name vocabulary: the string constants every package uses when it puts a value on a span or a log line.
Package logging is the Logger seam every package in this module writes through, and the noop that stands in when a caller supplies none.
Package logging is the Logger seam every package in this module writes through, and the noop that stands in when a caller supplies none.
config
Package loggingcfg selects and builds a logging.Logger from configuration: zerolog, zap, slog, the OTel-exporting slog, or none at all.
Package loggingcfg selects and builds a logging.Logger from configuration: zerolog, zap, slog, the OTel-exporting slog, or none at all.
noop
Package noop is the logging.Logger that writes nowhere.
Package noop is the logging.Logger that writes nowhere.
otelgrpc
Package otelgrpc implements logging.Logger over log/slog, fanning every record out to both stdout and an OTLP collector reached over gRPC.
Package otelgrpc implements logging.Logger over log/slog, fanning every record out to both stdout and an OTLP collector reached over gRPC.
slog
Package slog implements logging.Logger over the standard library's log/slog, emitting JSON to stdout.
Package slog implements logging.Logger over the standard library's log/slog, emitting JSON to stdout.
zap
Package zap implements logging.Logger over uber-go/zap.
Package zap implements logging.Logger over uber-go/zap.
zerolog
Package zerolog implements logging.Logger over rs/zerolog.
Package zerolog implements logging.Logger over rs/zerolog.
Package metrics provides a metrics-tracking implementation for the service.
Package metrics provides a metrics-tracking implementation for the service.
config
Package metricscfg selects and builds a metrics.Provider from configuration: the OTel gRPC exporter, or no metrics at all.
Package metricscfg selects and builds a metrics.Provider from configuration: the OTel gRPC exporter, or no metrics at all.
metricstest
Package metricstest provides metric instruments for tests.
Package metricstest provides metric instruments for tests.
mock
Package metricsmock provides moq-generated mocks for the metrics package.
Package metricsmock provides moq-generated mocks for the metrics package.
noop
Package noop is the metrics.Provider that exports nothing.
Package noop is the metrics.Provider that exports nothing.
otelgrpc
Package otelgrpc implements metrics.Provider against an OTLP collector reached over gRPC.
Package otelgrpc implements metrics.Provider against an OTLP collector reached over gRPC.
Package profiling is the seam for continuous profiling: the fourth observability pillar, and the one that is not a dependency of anything.
Package profiling is the seam for continuous profiling: the fourth observability pillar, and the one that is not a dependency of anything.
config
Package profilingcfg selects and builds a profiling.Provider from configuration: Grafana Pyroscope, the Go-native pprof HTTP server, or no profiling at all.
Package profilingcfg selects and builds a profiling.Provider from configuration: Grafana Pyroscope, the Go-native pprof HTTP server, or no profiling at all.
noop
Package noop is the profiling.Provider for a deployment that ships no profiles.
Package noop is the profiling.Provider for a deployment that ships no profiles.
pprof
Package pprof implements profiling.Provider by serving net/http/pprof from a dedicated HTTP server.
Package pprof implements profiling.Provider by serving net/http/pprof from a dedicated HTTP server.
pyroscope
Package pyroscope implements profiling.Provider by pushing profiles continuously to a Pyroscope server.
Package pyroscope implements profiling.Provider by pushing profiles continuously to a Pyroscope server.
Package tracing provides distributed tracing utilities.
Package tracing provides distributed tracing utilities.
cloudtrace
Package cloudtrace provides common functions for attaching values to trace spans
Package cloudtrace provides common functions for attaching values to trace spans
config
Package tracingcfg selects and builds a tracing.Provider from configuration: the OTel gRPC exporter, GCP Cloud Trace, or no tracing at all.
Package tracingcfg selects and builds a tracing.Provider from configuration: the OTel gRPC exporter, GCP Cloud Trace, or no tracing at all.
noop
Package noop is the tracing.Provider that records no spans, and the detail worth knowing about it is that it still propagates.
Package noop is the tracing.Provider that records no spans, and the detail worth knowing about it is that it still propagates.
oteltrace
Package oteltrace provides common functions for attaching values to trace spans
Package oteltrace provides common functions for attaching values to trace spans
Package o11yutils offers observability utility functions.
Package o11yutils offers observability utility functions.

Jump to

Keyboard shortcuts

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