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 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 "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())
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

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.