metrics

metrics defines a backend-agnostic instrumentation contract, with a no-op default that lets a service run fully wired before any backend is chosen.

Part of nurago, a collection of independent Go packages for backend services.

import "github.com/tecnickcom/nurago/pkg/metrics"

Defines the Client interface that service code instruments against, so the choice of metrics backend stays in main. Ships one no-op implementation; the real backends are opentel, prometheus and statsd.

The interface

Client covers the instrumentation points a service actually has, rather than exposing a generic counter/gauge/histogram vocabulary:

SqlOpen(driverName, dsn string) (*sql.DB, error)
InstrumentDB(name string, db *sql.DB) error
InstrumentHandler(path string, handler http.HandlerFunc) http.Handler
InstrumentRoundTripper(next http.RoundTripper) http.RoundTripper
MetricsHandlerFunc() http.HandlerFunc
IncLogLevelCounter(level string)
IncErrorCounter(task, operation, code string)
Close() error

Naming the integration points rather than the primitives is what makes the abstraction hold. A generic Counter(name string) interface leaks immediately, because Prometheus wants label dimensions declared up front, StatsD wants a dotted name, and OpenTelemetry wants attributes on an instrument. Wrapping an http.Handler is the same operation in all three.

SqlOpen sits in the interface because database instrumentation has to happen at connection time, not afterwards.

The no-op default is the useful part

Default implements every method as a no-op, with two exceptions that keep it usable rather than merely present:

  • SqlOpen returns a real, working *sql.DB. A service using the default client still has a database.
  • MetricsHandlerFunc returns a handler answering OK, so the metrics route exists and responds instead of 404ing.

This means a service can be fully wired for instrumentation from day one and run without any backend configured. Development, tests, and CLI tools all get the production code path with nothing to set up.

The alternative shapes are worse. A nil client means a nil check at every instrumentation site. Making the backend mandatory means tests need a metrics server.

The counters that are not derived

Two application-level counters are in the interface because nothing else can produce them:

IncLogLevelCounter(level) counts log records by severity. A rising rate of error records is a signal available before anyone reads the log, and it is the metric that most often catches a partial failure that has not yet affected latency or status codes.

IncErrorCounter(task, operation, code) records errors along three dimensions, which is enough to answer “which operation, in which task, failed how” without a log query.

logutil can drive the first one, so log-level counts stay consistent with what was actually logged.

Swapping backends

Application code depends on metrics.Client. Choosing a backend is one construction in main:

m, err := prometheus.New()          // scrape-based
m, err := statsd.New()              // push-based
m, err := opentel.New(ctx, name, version)

bootstrap takes the client and passes it to your bind function, so nothing below main knows which one is in use.

Close is in the interface because push-based backends buffer. Skipping it loses whatever had not yet flushed.

Dependencies

This package reaches no external module: it uses only the Go standard library. The backend packages carry their own.