Skip to content

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))
  • you are wiring a Recorder or AggregateCollector into 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
  • 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”
  1. metrics/module.yaml
  2. metrics/collector.go
  3. metrics/http_observer.go
  4. metrics/multi.go
  5. metrics/noop.go
Keep it in metrics when the work is aboutMove out when the work becomes
Recorder interface — the stable contract for recording observationsfeature-specific observer interfaces with domain-shaped record builders
AggregateCollector — composing multiple collectors without policyrolling-window aggregation, record-buffer retention, or adaptive sampling
HTTPObserver — recording HTTP request events through the stable interfacePrometheus histogram wiring, tracing spans, or transport-policy ownership
MultiCollector — fan-out compositionparallel identity fields, feature taxonomies, or devtools helpers
NoopCollector — zero-cost stand-in for testsrepo-wide metrics test utilities or mock injection helpers
import "github.com/spcent/plumego/metrics"

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}
}
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
}

HTTPObserver is the interface used by middleware/httpmetrics:

col := metrics.NewBaseMetricsCollector()
// Wire to httpmetrics middleware
import "github.com/spcent/plumego/middleware/httpmetrics"
app.Use(httpmetrics.Middleware(col))
// Or record manually inside a handler
col.ObserveHTTP(ctx, r.Method, r.URL.Path, status, responseBytes, duration)
col := metrics.NewMultiCollector(
prometheusCollector, // x/observability adapter
metrics.NewBaseMetricsCollector(), // in-memory baseline
)
svc := NewUserService(db, metrics.NewNoopCollector())
InterfacePrimary use
metrics.RecorderAccept in components that record observations
metrics.AggregateCollectorAccept when composing or passing a full collector
metrics.HTTPObserverAccept in HTTP-layer components that observe requests

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.