Metrics Primer
Metrics Primer
Section titled “Metrics Primer”Open this page after Stable Roots when the change still belongs to the default service path and the real question has narrowed to how the service records observations: what interface a component publishes metrics through, how base collectors are composed, and what HTTP observation looks like.
metrics owns the Recorder and AggregateCollector interfaces, HTTPObserver, BaseMetricsCollector, NoopCollector, and MultiCollector. It imports only log. Export pipelines, Prometheus adapters, rolling-window aggregation, and test utilities belong in x/observability.
collector := metrics.NewBaseMetricsCollector()app.Use(httpmetrics.Middleware(collector))Start here when
Section titled “Start here when”- you are wiring a
RecorderorAggregateCollectorinto a component or handler - you are recording an HTTP observation via
HTTPObserver - you are composing multiple collectors with
MultiCollector - you are standing in for a real collector in tests with
NoopCollector - you are implementing a new base collector satisfying the stable interfaces
Do not start here when
Section titled “Do not start here when”- the change adds Prometheus export, tracing, or rolling-window aggregation — that belongs in
x/observability - the change introduces feature-specific dashboard fields, record-buffer retention, or per-feature metric type catalogs
- the work is about transport policy, request correlation, or middleware-owned observation shapes
- the change adds a repo-wide metrics test utility or devtools helper
First files to read in the current repository
Section titled “First files to read in the current repository”metrics/module.yamlmetrics/collector.gometrics/http_observer.gometrics/multi.gometrics/noop.go
Concrete ownership examples
Section titled “Concrete ownership examples”Keep it in metrics when the work is about | Move out when the work becomes |
|---|---|
Recorder interface — the stable contract for recording observations | feature-specific observer interfaces with domain-shaped record builders |
AggregateCollector — composing multiple collectors without policy | rolling-window aggregation, record-buffer retention, or adaptive sampling |
HTTPObserver — recording HTTP request events through the stable interface | Prometheus histogram wiring, tracing spans, or transport-policy ownership |
MultiCollector — fan-out composition | parallel identity fields, feature taxonomies, or devtools helpers |
NoopCollector — zero-cost stand-in for tests | repo-wide metrics test utilities or mock injection helpers |
Import
Section titled “Import”import "github.com/spcent/plumego/metrics"Inject a collector
Section titled “Inject a collector”Accept metrics.AggregateCollector at dependency boundaries. Use metrics.NewNoopCollector() in tests and pass a real collector in production:
type UserRepository interface { Create(ctx context.Context, req CreateRequest) (*User, error)}
type UserService struct { repo UserRepository collector metrics.AggregateCollector}
func NewUserService(repo UserRepository, col metrics.AggregateCollector) *UserService { return &UserService{repo: repo, collector: col}}Record a custom observation
Section titled “Record a custom observation”import "github.com/spcent/plumego/metrics"
func (s *UserService) Create(ctx context.Context, req CreateRequest) (*User, error) { start := time.Now() user, err := s.doCreate(ctx, req)
s.collector.Record(ctx, metrics.MetricRecord{ Name: "user.create", Value: time.Since(start).Seconds(), Duration: time.Since(start), Labels: metrics.MetricLabels{"status": statusLabel(err)}, Error: err, }) return user, err}Record an HTTP observation
Section titled “Record an HTTP observation”HTTPObserver is the interface used by middleware/httpmetrics:
col := metrics.NewBaseMetricsCollector()
// Wire to httpmetrics middlewareimport "github.com/spcent/plumego/middleware/httpmetrics"app.Use(httpmetrics.Middleware(col))
// Or record manually inside a handlercol.ObserveHTTP(ctx, r.Method, r.URL.Path, status, responseBytes, duration)Fan-out to multiple collectors
Section titled “Fan-out to multiple collectors”col := metrics.NewMultiCollector( prometheusCollector, // x/observability adapter metrics.NewBaseMetricsCollector(), // in-memory baseline)Noop collector for tests
Section titled “Noop collector for tests”svc := NewUserService(db, metrics.NewNoopCollector())Interfaces at a glance
Section titled “Interfaces at a glance”| Interface | Primary use |
|---|---|
metrics.Recorder | Accept in components that record observations |
metrics.AggregateCollector | Accept when composing or passing a full collector |
metrics.HTTPObserver | Accept in HTTP-layer components that observe requests |
Why this primer exists
Section titled “Why this primer exists”metrics sits at the same level as log: it defines the contract but not the destination. Keeping export wiring out of the stable layer means you can swap backends (Prometheus, OpenTelemetry, custom) without changing anything that imports metrics. The boundary also prevents feature teams from accumulating domain-specific metric types in the stable package, which would turn a small contract into an ever-growing catalog.