metrics

package
v2.0.0 Latest Latest
Warning

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

Go to latest
Published: Apr 20, 2026 License: MIT Imports: 8 Imported by: 0

README

metrics

Package metrics provides an isolated Prometheus registry that integrates with the LabKit v2 component lifecycle. There is no shared global state — each Metrics instance owns its own prometheus.Registry, so components and tests never interfere with each other.

Quick start

m, err := metrics.New()
if err != nil {
    log.Fatal(err)
}

a.Register(m) // Start registers go_ and process_ collectors; Shutdown is a no-op

// Mount alongside the existing health endpoints.
srv.Router().Get("/-/metrics", m.Handler())

Defining metrics

Use BuildName to prefix metric names with the configured namespace (default "gitlab"), then register with MustRegister:

requestsTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
    Name: m.BuildName("http", "requests_total"),
    Help: "Total number of HTTP requests partitioned by feature category and status.",
}, []string{metrics.LabelFeatureCategory, metrics.LabelStatus})

m.MustRegister(requestsTotal)

// Increment on every request:
requestsTotal.WithLabelValues("code_review", "2xx").Inc()

BuildName(subsystem, name) follows the Prometheus convention of joining parts with underscores, omitting empty segments:

Call Result
m.BuildName("http", "requests_total") gitlab_http_requests_total
m.BuildName("worker", "jobs_total") gitlab_worker_jobs_total
m.BuildName("", "up") gitlab_up

Standard labels

Use the label name constants to keep dashboards and alert rules consistent across services:

Constant Value Use for
LabelComponent "component" Logical sub-system, e.g. "api", "sidekiq"
LabelFeatureCategory "feature_category" Handbook feature category for SLO ownership
LabelEndpointID "endpoint_id" Low-cardinality endpoint identifier, e.g. "GET /api/v4/projects/{id}"
LabelStatus "status" HTTP response status class, e.g. "2xx" or "5xx"

Standard bucket sets

DurationBuckets provides SLO-aligned histogram boundaries for HTTP and RPC latency (100ms to 60s). The five boundaries include 1 (satisfied threshold) and 10 (tolerated threshold) to match the Workhorse and Rails SLI thresholds used across GitLab's metrics catalog:

prometheus.NewHistogramVec(prometheus.HistogramOpts{
    Name:    m.BuildName("http", "request_duration_seconds"),
    Buckets: metrics.DurationBuckets,
}, []string{metrics.LabelEndpointID, metrics.LabelStatus})

HTTP service example

The pattern below wires Metrics into an httpserver.Server. Metrics are defined once at construction time and incremented inside the handler closure:

func main() {
    ctx := context.Background()

    a, _ := app.New(ctx)

    m, _ := metrics.New()
    a.Register(m)

    // Define metrics before Start is called.
    requestsTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
        Name: m.BuildName("http", "requests_total"),
        Help: "Total HTTP requests partitioned by endpoint and status.",
    }, []string{metrics.LabelEndpointID, metrics.LabelStatus})

    requestDuration := prometheus.NewHistogramVec(prometheus.HistogramOpts{
        Name:    m.BuildName("http", "request_duration_seconds"),
        Help:    "HTTP request latency.",
        Buckets: metrics.DurationBuckets,
    }, []string{metrics.LabelEndpointID})

    m.MustRegister(requestsTotal, requestDuration)

    srv := httpserver.NewWithConfig(&httpserver.Config{
        Addr:   ":8080",
        Tracer: a.Tracer(),
    })

    srv.Router().Get("/api/projects/{id}", func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        // ... handler logic ...

        requestsTotal.WithLabelValues("GET /api/projects/{id}", "2xx").Inc()
        requestDuration.WithLabelValues("GET /api/projects/{id}").Observe(time.Since(start).Seconds())

        w.WriteHeader(http.StatusOK)
    })

    // Expose metrics at /-/metrics alongside /-/liveness and /-/readiness.
    srv.Router().Get("/-/metrics", m.Handler())

    a.Register(srv)
    a.Run(ctx)
}

Note: for high-traffic services consider using prometheus.NewHistogramVec with NativeHistogramBucketFactor instead of fixed Buckets to reduce cardinality and scrape payload size.

Configuration

m, err := metrics.NewWithConfig(&metrics.Config{
    Name:      "primary-metrics", // component name in logs (default: "metrics")
    Namespace: "myservice",       // metric name prefix   (default: "gitlab")
})

When writing tests, pass a fresh prometheus.Registry to isolate the metric state completely (see Testing below).

Injecting into sub-components

Pass only the prometheus.Registerer interface to components that should register their own collectors but not control the full Metrics instance. This limits the API surface and makes dependencies explicit:

type Worker struct {
    jobsTotal prometheus.Counter
}

func NewWorker(reg prometheus.Registerer, m *metrics.Metrics) (*Worker, error) {
    c := prometheus.NewCounter(prometheus.CounterOpts{
        Name: m.BuildName("worker", "jobs_total"),
        Help: "Total jobs processed.",
    })
    if err := reg.Register(c); err != nil {
        return nil, err
    }
    return &Worker{jobsTotal: c}, nil
}

// Wire up:
worker, err := NewWorker(m.Registerer(), m)
a.Register(worker)

Testing

Use metricstest.New for an isolated Metrics backed by a fresh registry. The component is not started — Go runtime and process collectors are absent, keeping gathered output minimal and assertions deterministic:

import "gitlab.com/gitlab-org/labkit/v2/testing/metricstest"

func TestWorker_countsJobs(t *testing.T) {
    m := metricstest.New(t)

    worker, err := NewWorker(m.Registerer(), m)
    require.NoError(t, err)
    require.NoError(t, worker.ProcessJob(ctx))

    families := metricstest.Gather(t, m)

    require.Contains(t, families, "gitlab_worker_jobs_total")
    assert.Equal(t, 1.0, families["gitlab_worker_jobs_total"].Metric[0].Counter.GetValue())
}

metricstest.Gather returns a map[string]*dto.MetricFamily keyed by fully-qualified metric name, so you can assert on counters, gauges, and histogram observations without a running Prometheus server.

Documentation

Overview

Package metrics provides an isolated Prometheus registry that integrates with the LabKit v2 component lifecycle.

All metrics are registered against a private prometheus.Registry — there is no shared global state between components or tests. The Metrics type implements [app.Component]: Metrics.Start registers the standard Go runtime and process collectors that all GitLab services are expected to export; Metrics.Handler returns an HTTP handler ready to mount at /-/metrics.

Quick start

m, err := metrics.New()
if err != nil {
    log.Fatal(err)
}
a.Register(m)

srv.Router().Get("/-/metrics", m.Handler())

Defining metrics

Use Metrics.BuildName to prefix metric names with the configured namespace (default "gitlab"), then register the collector with Metrics.MustRegister:

requestsTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
    Name: m.BuildName("http", "requests_total"),
    Help: "Total number of HTTP requests.",
}, []string{metrics.LabelFeatureCategory, metrics.LabelStatus})

m.MustRegister(requestsTotal)

// Increment on every request:
requestsTotal.WithLabelValues("code_review", "2xx").Inc()

Standard labels

Use the label name constants to avoid typos and keep dashboards consistent across services:

prometheus.Labels{
    metrics.LabelComponent:       "api",
    metrics.LabelFeatureCategory: "merge_requests",
}

Standard bucket sets

DurationBuckets provides SLO-aligned histogram boundaries for HTTP and RPC latency (seconds). The five boundaries include 1 (satisfied threshold) and 10 (tolerated threshold) to match the Workhorse and Rails SLI thresholds.

latency := prometheus.NewHistogramVec(prometheus.HistogramOpts{
    Name:    m.BuildName("http", "request_duration_seconds"),
    Help:    "HTTP request latency.",
    Buckets: metrics.DurationBuckets,
}, []string{metrics.LabelEndpointID, metrics.LabelStatus})

Injecting into sub-components

Pass only the prometheus.Registerer interface to components that need to register their own collectors. This limits access and keeps the public API surface small:

type Worker struct { reg prometheus.Registerer }

func (w *Worker) Start(ctx context.Context) error {
    jobs := prometheus.NewCounter(...)
    return w.reg.Register(jobs)
}

worker := &Worker{reg: m.Registerer()}
a.Register(worker)

Testing

Use [metricstest.New] from gitlab.com/gitlab-org/labkit/v2/testing/metricstest to obtain an isolated Metrics instance backed by a fresh registry. [metricstest.Gather] returns all metric families keyed by name for assertion:

import "gitlab.com/gitlab-org/labkit/v2/testing/metricstest"

func TestWorker_countsJobs(t *testing.T) {
    m := metricstest.New(t)

    w := NewWorker(m.Registerer())
    require.NoError(t, w.ProcessJob(ctx))

    families := metricstest.Gather(t, m)
    require.Contains(t, families, "gitlab_worker_jobs_total")
    assert.Equal(t, 1, int(families["gitlab_worker_jobs_total"].Metric[0].Counter.GetValue()))
}
Example

Example shows a Metrics component registered with an app.App lifecycle. In production use app.Run instead of Start/Shutdown directly.

package main

import (
	"context"

	"gitlab.com/gitlab-org/labkit/v2/metrics"
)

func main() {
	ctx := context.Background()

	m, err := metrics.New()
	if err != nil {
		panic(err)
	}

	if err := m.Start(ctx); err != nil {
		panic(err)
	}
	defer m.Shutdown(ctx)

	// Mount the handler at /-/metrics in your HTTP server.
	_ = m.Handler()
}

Index

Examples

Constants

View Source
const (
	// LabelComponent identifies the logical component within a service,
	// e.g. "gitaly_pack_objects" or "workhorse_git_http".
	LabelComponent = "component"

	// LabelFeatureCategory maps a metric to a GitLab feature category for
	// ownership attribution, error budget tracking, and SLO alerting.
	// Values should match the feature category taxonomy in the GitLab handbook.
	LabelFeatureCategory = "feature_category"

	// LabelEndpointID identifies the endpoint that handled a request using a
	// low-cardinality identifier, e.g. "GET /api/v4/projects/{id}". Prefer
	// this over separate controller/action/route labels to reduce cardinality.
	LabelEndpointID = "endpoint_id"

	// LabelStatus is the HTTP response status class, e.g. "2xx" or "5xx".
	// Always use a class rather than the exact code to keep cardinality low.
	LabelStatus = "status"
)

Standard Prometheus label names used consistently across GitLab services. Using these constants avoids typos and ensures that dashboards and alert rules written against one service work uniformly across the fleet.

Variables

View Source
var DurationBuckets = []float64{0.1, 0.5, 1, 10, 60}

DurationBuckets are histogram bucket boundaries (in seconds) for measuring HTTP request latency in SLO-oriented histograms. The boundaries are tuned to align with Workhorse and Rails SLI thresholds used across GitLab's metrics catalog: 1s is the satisfied threshold and 10s is the tolerated threshold for most services.

Functions

This section is empty.

Types

type Config

type Config struct {
	// Name identifies this component in logs and errors.
	// Defaults to "metrics".
	Name string

	// Namespace is prepended to every metric name built with [Metrics.BuildName],
	// following the Prometheus convention of namespace_subsystem_name.
	// Defaults to "gitlab" to match GitLab's metric naming standards.
	Namespace string

	// Registry is the Prometheus registry used for all collector registration
	// and metric gathering. When nil, a new isolated registry is created.
	//
	// Override in tests to inject a pre-populated or inspectable registry
	// without affecting the process-global default.
	Registry *prometheus.Registry
}

Config holds optional configuration for New / NewWithConfig.

type Metrics

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

Metrics is an isolated Prometheus registry that implements [app.Component].

Use Metrics.MustRegister or Metrics.Register to add application or component-specific collectors. Use Metrics.Registerer when passing the registry to sub-components that should not control the full Metrics instance. Use Metrics.Handler to expose the collected metrics over HTTP.

func New

func New() (*Metrics, error)

New returns a Metrics with default configuration.

func NewWithConfig

func NewWithConfig(cfg *Config) (*Metrics, error)

NewWithConfig returns a Metrics configured with cfg.

Example

ExampleNewWithConfig shows how to configure a custom namespace and register application-specific collectors.

package main

import (
	"github.com/prometheus/client_golang/prometheus"
	"gitlab.com/gitlab-org/labkit/v2/metrics"
)

func main() {
	m, err := metrics.NewWithConfig(&metrics.Config{
		Name:      "primary-metrics",
		Namespace: "myservice",
	})
	if err != nil {
		panic(err)
	}

	requestsTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
		Name: m.BuildName("http", "requests_total"),
		Help: "Total number of HTTP requests.",
		ConstLabels: prometheus.Labels{
			metrics.LabelComponent: "api",
		},
	}, []string{metrics.LabelFeatureCategory, metrics.LabelStatus})

	m.MustRegister(requestsTotal)

	requestsTotal.WithLabelValues("code_review", "2xx").Inc()
}

func (*Metrics) BuildName

func (m *Metrics) BuildName(subsystem, name string) string

BuildName returns a fully-qualified metric name by joining the configured namespace, an optional subsystem, and the metric name, separated by underscores. Pass an empty subsystem to omit it.

m.BuildName("http", "requests_total")  // → gitlab_http_requests_total
m.BuildName("", "up")                  // → gitlab_up
Example

ExampleMetrics_BuildName shows how BuildName produces fully-qualified metric names from a namespace, subsystem, and name.

package main

import (
	"gitlab.com/gitlab-org/labkit/v2/metrics"
)

func main() {
	m, _ := metrics.NewWithConfig(&metrics.Config{Namespace: "gitlab"})

	_ = m.BuildName("workhorse", "requests_total") // → gitlab_workhorse_requests_total
	_ = m.BuildName("", "up")                      // → gitlab_up
}

func (*Metrics) Gatherer

func (m *Metrics) Gatherer() prometheus.Gatherer

Gatherer returns the prometheus.Gatherer for the underlying registry. Useful when combining multiple registries into a single metrics endpoint.

func (*Metrics) Handler

func (m *Metrics) Handler() http.Handler

Handler returns an http.Handler that serves all collected metrics in the Prometheus text exposition format. Mount it at /-/metrics to follow GitLab's health endpoint conventions alongside /-/liveness and /-/readiness.

The handler itself registers a small number of internal metrics (request counts and in-flight gauges) against the same registry so they appear in the output alongside application metrics.

Example

ExampleMetrics_Handler shows how to wire the metrics handler into an HTTP server and exercise it in a test without binding a real port.

package main

import (
	"net/http"
	"net/http/httptest"

	"github.com/prometheus/client_golang/prometheus"
	"gitlab.com/gitlab-org/labkit/v2/metrics"
)

func main() {
	m, _ := metrics.New()

	hits := prometheus.NewCounter(prometheus.CounterOpts{
		Name: m.BuildName("", "cache_hits_total"),
		Help: "Total number of cache hits.",
	})
	m.MustRegister(hits)
	hits.Add(7)

	req := httptest.NewRequest(http.MethodGet, "/-/metrics", nil)
	rec := httptest.NewRecorder()
	m.Handler().ServeHTTP(rec, req)

	_ = rec.Code // 200
}

func (*Metrics) MountOn

func (m *Metrics) MountOn(r RouteRegistrar)

MountOn registers the metrics handler at /-/metrics on r. Use this when building a custom HTTP server that does not use the httpserver package, which mounts the endpoint automatically when a Metrics instance is configured.

func (*Metrics) MustRegister

func (m *Metrics) MustRegister(cs ...prometheus.Collector)

MustRegister registers collectors with the underlying registry. It panics on any registration error, consistent with the prometheus MustRegister convention used at init time.

func (*Metrics) Name

func (m *Metrics) Name() string

Name returns the component name for use in logs and error messages.

func (*Metrics) Register

func (m *Metrics) Register(c prometheus.Collector) error

Register registers a collector with the underlying registry.

func (*Metrics) Registerer

func (m *Metrics) Registerer() prometheus.Registerer

Registerer returns the prometheus.Registerer for the underlying registry. Pass this to components that need to register their own collectors without receiving access to the full Metrics instance.

func (*Metrics) Shutdown

func (m *Metrics) Shutdown(_ context.Context) error

Shutdown is a no-op; Prometheus registries hold no closeable resources. It satisfies [app.Component] and should be called via [app.App.Shutdown].

func (*Metrics) Start

func (m *Metrics) Start(_ context.Context) error

Start registers the Go runtime and process collectors with the underlying registry. It satisfies [app.Component] and should be called via [app.App.Start].

type RouteRegistrar

type RouteRegistrar interface {
	Handle(pattern string, handler http.Handler)
}

RouteRegistrar is a minimal interface for mounting an http.Handler at a path pattern. It is satisfied by net/http.ServeMux, the httpserver.Router interface, and chi routers, allowing MountOn to work with any HTTP framework.

Jump to

Keyboard shortcuts

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