metrics

package
v1.2.6 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: GPL-3.0 Imports: 7 Imported by: 0

README

metrics

Import path: github.com/InsideGallery/core/metrics

metrics provides backend-agnostic service instrumentation. Services record counts, gauges, and distributions through a Client; processor packages register concrete exporters by name.

Main APIs

  • Config selects processors.
  • GetEnvConfig(prefix ...string) reads metrics config, defaulting to the METRICS prefix.
  • PrometheusOnly(cfg Config) collapses any enabled config to Prometheus.
  • Processor is the exporter interface: Close, Count, Gauge, and Distribution.
  • Register, RegisteredProcessors, and Factory manage processor registration.
  • New(cfg Config, service string) builds a fanout client.
  • Default, SetDefault, and InstallDefault manage the process-wide client.
  • NormalizeTags returns a sorted copy of tags; TagSet joins sorted tags with commas.
  • Counter, Gauge, and Observer are resolved metric handles; HandleProvider is the optional capability of resolving one, implemented by *Client and by the Prometheus processor.
  • (*Client).CounterHandle, GaugeHandle, and DistributionHandle resolve a handle across every configured processor.
  • SeriesDeleter is the optional capability of retiring a published series, implemented by *Client and by the Prometheus processor; (*Client).DeleteSeriesMatching fans the delete out to whichever processors have it.
  • ErrSeriesDeleteUnsupported and ErrEmptySeriesMatch are the two refusals a delete can report.

Usage

package example

import (
	"errors"

	_ "github.com/InsideGallery/core/metrics/all"

	"github.com/InsideGallery/core/metrics"
)

func recordMetric() (err error) {
	cfg, err := metrics.GetEnvConfig()
	if err != nil {
		return err
	}

	client, err := metrics.New(cfg, "api")
	if err != nil {
		return err
	}
	if client == nil {
		return nil
	}

	handle := metrics.InstallDefault(client)
	defer func() {
		err = errors.Join(err, handle.Close())
	}()

	return client.Count("requests_total", 1, []string{"status:ok"})
}

Configuration

GetEnvConfig reads:

  • METRICS_PROCESSORS: comma-separated processor names, default prometheus.

Processor names are trimmed, lowercased, de-duplicated, and may be split across comma-separated entries. The values none, off, and disabled disable metrics. Processor-specific environment variables do not select processors; they only configure a processor after it has been selected and registered.

Resolved Handles

Count, Gauge, and Distribution take a (name, tags) tuple and every processor has to turn it back into a backend child metric on each record. A caller on a data path records the same bounded set of tuples forever, so it can resolve the child once instead:

requests, err := client.CounterHandle("requests_total", []string{"op:get", "status:ok"})
if err != nil {
	return err
}

requests.Add(1) // per operation: no tuple, no lookup

Against the Prometheus processor, whose memo already makes a repeat record allocation-free, this is 71.9ns -> 6.8ns per record (BenchmarkResolvedHandleVersusMemoizedRecord, medians of 5, one process, 0 allocations in both arms).

HandleProvider is deliberately not part of Processor. A processor that does not implement it keeps working: *Client adapts its Count/Gauge/Distribution methods into a handle, so datadog, otel, and statsd need no change, and a client with a mix of processors records into all of them through one handle. A resolution error is returned rather than absorbed — the caller has no handle and can still record through Count.

Counter, Gauge, and Observer are aliases to interface literals (interface{ Add(value int64) } and peers), not defined types. That is what lets a consumer pinned to a release without this API declare the same literal locally and detect the capability with a type assertion, using handles when the linked version provides them and Count/Distribution when it does not. Do not turn them into defined types: Go matches method signatures on type identity, and a defined type is never identical to any other type, so consumers would have to bump their pin in lockstep.

Series Retirement

A backend that holds its series in-process keeps exporting one after the last record: the value freezes and the series stays on every scrape until the process exits. Where the label identifies something that comes and goes — a peer address, a pod IP, a tenant — that is unbounded cardinality growth in dead series, and a frozen counter reads to an operator like an active fault. DeleteSeriesMatching removes the children themselves:

if err := client.DeleteSeriesMatching("fabric_connection_errors_total", []string{"peer:" + address}); err != nil {
	slog.Warn("retire peer series", "peer", address, "error", err)
}

The match is partial: tags names the labels that identify the subject, and every child carrying them is deleted whatever its other labels hold. That is deliberate — a series split by an open-ended label (an error reason, a status class) has children the caller cannot enumerate, and retiring the subject has to take all of them. An empty match would select every child of the metric, so it is refused with ErrEmptySeriesMatch rather than obeyed; the case that matters is not a caller typing nil but a caller assembling a tag from an empty subject.

SeriesDeleter is not part of Processor, for the same reason HandleProvider is not — but unlike a handle it cannot be adapted, because a push backend has no resident series to retire and the OpenTelemetry SDK exposes no removal at all. A processor without the capability is therefore skipped rather than failed. A client where no processor can delete reports ErrSeriesDeleteUnsupported, so a caller learns the label it wanted gone is still being exported instead of assuming success.

Two rules for callers:

  • Retire only what has genuinely gone away. A counter for a subject that is merely unreachable is exactly the signal an operator needs during an incident, and deleting it destroys that signal at the moment it matters. Where the same subject can come back at the same identity, treat its series as a counter reset — do not write absent() alerts on them.
  • Retire series recorded through Count, Gauge, and Distribution. Those resolve their backend child per record and recreate it, so a returning subject counts from zero with no residue. A handle resolved through HandleProvider holds the child directly, so deleting that child orphans the handle: records through it are accepted and exported nowhere. Re-resolve the handle after retiring, or keep a retirable series on the recording path.

Operational Notes

New returns nil, nil when metrics are disabled. A nil *Client is safe to call and returns nil for Close, Count, Gauge, and Distribution.

Import metrics/all or the specific processor packages before selecting processor names in METRICS_PROCESSORS. Processor call errors are joined and wrapped with the metric operation and name.

Documentation

Overview

Package metrics provides backend-agnostic service instrumentation.

Services record metrics through Client or the Processor interface. Concrete exporters live in pkg/metrics/processors/* and register themselves at init, following the same plugin pattern used by pkg/fastlog.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrSeriesDeleteUnsupported reports that none of the configured processors can
	// retire a series, so the series a caller asked to delete is still published.
	// It is returned rather than absorbed because the caller cannot tell otherwise:
	// a successful delete and a delete nobody could perform both leave it with no
	// error and no way to know the label it wanted gone is still being exported.
	ErrSeriesDeleteUnsupported = errors.New("no configured processor can delete a series")

	// ErrEmptySeriesMatch reports a delete whose tags named no label. An empty match
	// selects every child of the metric, so it is refused instead of being obeyed:
	// wiping a whole metric family is not what a caller who passed no tags — or
	// passed one assembled from an empty subject — meant to ask for.
	ErrEmptySeriesMatch = errors.New("series delete requires at least one key:value tag")
)

Functions

func NormalizeTags

func NormalizeTags(tags []string) []string

NormalizeTags returns a stable copy of tags suitable for processors.

func Register

func Register(kind string, factory Factory)

Register makes a metrics processor available by kind.

func RegisteredProcessors

func RegisteredProcessors() []string

RegisteredProcessors returns all registered processor names.

func SetDefault

func SetDefault(c *Client)

SetDefault stores the process-wide metrics client for service-specific instrumentation.

func TagSet

func TagSet(tags []string) string

TagSet returns a stable tag-set strings for processors that cannot model arbitrary labels.

Types

type Client

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

Client fans metric calls out to configured processors.

func Default

func Default() *Client

Default returns the process-wide metrics client, or nil when metrics are disabled.

func New

func New(cfg Config, service string) (*Client, error)

New creates a metrics client from configured processors. Returns nil if cfg is not enabled.

func (*Client) Close

func (c *Client) Close() error

Close flushes pending metrics and closes all processors.

func (*Client) Count

func (c *Client) Count(name string, value int64, tags []string) error

Count records a count metric.

func (*Client) CounterHandle added in v1.2.5

func (c *Client) CounterHandle(name string, tags []string) (Counter, error)

CounterHandle resolves one counter handle for the metric, covering every configured processor. See HandleProvider.

func (*Client) DeleteSeriesMatching added in v1.2.6

func (c *Client) DeleteSeriesMatching(name string, tags []string) error

DeleteSeriesMatching retires the series in every configured processor that can retire one. See SeriesDeleter for the match semantics and for when retiring is the right thing to do at all.

A processor without the capability is skipped rather than reported: it holds no resident series, so there is nothing there to retire and its presence in the client is not a failure. Only a client where no processor at all can delete reports ErrSeriesDeleteUnsupported.

func (*Client) Distribution

func (c *Client) Distribution(name string, value float64, tags []string) error

Distribution records a distribution metric.

func (*Client) DistributionHandle added in v1.2.5

func (c *Client) DistributionHandle(name string, tags []string) (Observer, error)

DistributionHandle resolves one distribution handle for the metric, covering every configured processor. See HandleProvider.

func (*Client) Gauge

func (c *Client) Gauge(name string, value float64, tags []string) error

Gauge records a gauge metric.

func (*Client) GaugeHandle added in v1.2.5

func (c *Client) GaugeHandle(name string, tags []string) (Gauge, error)

GaugeHandle resolves one gauge handle for the metric, covering every configured processor. See HandleProvider.

type Config

type Config struct {
	Processors []string `env:"_PROCESSORS" envDefault:"prometheus"`
}

Config holds backend-agnostic metrics configuration.

Environment:

  • METRICS_PROCESSORS defaults to prometheus.

func GetEnvConfig

func GetEnvConfig(prefix ...string) (Config, error)

GetEnvConfig reads metrics configuration from environment variables. Default prefix is METRICS. Processor-specific packages own their own env config.

func PrometheusOnly added in v1.2.1

func PrometheusOnly(cfg Config) Config

PrometheusOnly returns a config that uses Prometheus for every enabled metrics setup.

func (Config) Enabled

func (c Config) Enabled() bool

Enabled reports whether any processor is configured.

func (Config) EnabledProcessors

func (c Config) EnabledProcessors() []string

EnabledProcessors returns the configured processors.

type Counter added in v1.2.5

type Counter = interface{ Add(value int64) }

Counter, Gauge and Observer are resolved metric handles: a caller resolves one through HandleProvider at wiring time and records through it afterwards, so turning a (name, tags) tuple back into a backend child metric happens once instead of on every record. On a data path that records the same bounded set of tuples forever, that is the difference between a hashed cache lookup per record and an increment.

They are aliases to interface literals rather than defined types, and that is load-bearing. A consumer pinned to a release of this module that predates HandleProvider cannot name metrics.Counter, but it can declare the identical literal locally and assert the capability structurally, because Go matches method signatures on type identity and a defined type is never identical to any other type. The aliases are therefore what let a consumer use handles when the linked version provides them and keep using Count/Gauge/Distribution when it does not, without bumping its pin in lockstep with this addition. Do not turn them into defined types.

type DefaultHandle

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

DefaultHandle restores a package-level metrics default and closes its client.

func InstallDefault

func InstallDefault(c *Client) *DefaultHandle

InstallDefault installs a process-wide metrics default with an explicit close path.

func (*DefaultHandle) Client

func (h *DefaultHandle) Client() *Client

Client returns the installed default client.

func (*DefaultHandle) Close

func (h *DefaultHandle) Close() error

Close restores the previous default client and closes the installed client.

type Factory

type Factory func(Config, string) (Processor, error)

Factory creates a concrete metrics processor for a service.

type Gauge added in v1.2.5

type Gauge = interface{ Set(value float64) }

Counter, Gauge and Observer are resolved metric handles: a caller resolves one through HandleProvider at wiring time and records through it afterwards, so turning a (name, tags) tuple back into a backend child metric happens once instead of on every record. On a data path that records the same bounded set of tuples forever, that is the difference between a hashed cache lookup per record and an increment.

They are aliases to interface literals rather than defined types, and that is load-bearing. A consumer pinned to a release of this module that predates HandleProvider cannot name metrics.Counter, but it can declare the identical literal locally and assert the capability structurally, because Go matches method signatures on type identity and a defined type is never identical to any other type. The aliases are therefore what let a consumer use handles when the linked version provides them and keep using Count/Gauge/Distribution when it does not, without bumping its pin in lockstep with this addition. Do not turn them into defined types.

type HandleProvider added in v1.2.5

type HandleProvider interface {
	CounterHandle(name string, tags []string) (Counter, error)
	GaugeHandle(name string, tags []string) (Gauge, error)
	DistributionHandle(name string, tags []string) (Observer, error)
}

HandleProvider is the optional capability of resolving a metric handle before recording. It is deliberately not part of Processor: a processor that does not implement it keeps recording through Count, Gauge and Distribution, so implementing it stays opt-in per backend and callers detect support with a type assertion. A returned handle must be safe for concurrent use and stays valid for the life of the processor.

type Observer added in v1.2.5

type Observer = interface{ Observe(value float64) }

Counter, Gauge and Observer are resolved metric handles: a caller resolves one through HandleProvider at wiring time and records through it afterwards, so turning a (name, tags) tuple back into a backend child metric happens once instead of on every record. On a data path that records the same bounded set of tuples forever, that is the difference between a hashed cache lookup per record and an increment.

They are aliases to interface literals rather than defined types, and that is load-bearing. A consumer pinned to a release of this module that predates HandleProvider cannot name metrics.Counter, but it can declare the identical literal locally and assert the capability structurally, because Go matches method signatures on type identity and a defined type is never identical to any other type. The aliases are therefore what let a consumer use handles when the linked version provides them and keep using Count/Gauge/Distribution when it does not, without bumping its pin in lockstep with this addition. Do not turn them into defined types.

type Processor

type Processor interface {
	Close() error
	Count(name string, value int64, tags []string) error
	Gauge(name string, value float64, tags []string) error
	Distribution(name string, value float64, tags []string) error
}

Processor records metrics for one concrete backend.

type SeriesDeleter added in v1.2.6

type SeriesDeleter interface {
	DeleteSeriesMatching(name string, tags []string) error
}

SeriesDeleter is the optional capability of retiring a published series: delete every child of name whose labels include all of tags. Like HandleProvider it is deliberately not part of Processor — only a backend that holds its series in-process has anything to retire, so a push backend needs no change and callers detect support with a type assertion. *Client implements it and fans the delete out to whichever processors do.

The match is partial rather than exact because the labels that identify a subject are usually not all of its labels: a series split by an open-ended label (an error reason, a status class) has children a caller cannot enumerate, and retiring the subject has to retire all of them. An empty match is refused with ErrEmptySeriesMatch instead of matching everything.

Two rules for callers:

  • Retire only what has genuinely gone away. A counter for a subject that is merely unreachable is exactly the signal an operator needs during an incident, and deleting it destroys that signal at the moment it matters.
  • Retire series recorded through Count, Gauge and Distribution, which resolve their backend child per record and therefore recreate it. A handle resolved through HandleProvider holds the child directly, so deleting that child orphans the handle: records through it are accepted and exported nowhere. Give a retirable series the recording path, or re-resolve the handle afterwards.

Directories

Path Synopsis
Package all imports every in-tree metrics processor so each processor registers with the default metrics registry through its init hook.
Package all imports every in-tree metrics processor so each processor registers with the default metrics registry through its init hook.
processors

Jump to

Keyboard shortcuts

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