metrics

package
v1.8.2 Latest Latest
Warning

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

Go to latest
Published: Apr 16, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Index

Constants

View Source
const DefaultInterceptorLabelCacheLimit = 4096

DefaultInterceptorLabelCacheLimit is the per-cache cardinality cap applied to each of the four process-global interceptor label caches. The value is chosen well above any realistic server's RPC surface (a .proto surface with >1000 distinct methods is pathological) and comfortably below the point where the fall-through allocation cost would dominate. Four separate caches × 4096 entries × ~64 B/entry ≈ 1 MiB steady-state worst case — negligible.

On-overflow behavior: getOrBuild* returns a fresh per-call allocation and increments InterceptorLabelCacheOverflow(). The series remains observable downstream; only the hot-path optimization is dropped.

View Source
const DefaultMemorySinkLimit = 10_000

DefaultMemorySinkLimit is the cardinality cap applied by NewMemorySink unless the caller explicitly opts into NewMemorySinkUnbounded or overrides via NewMemorySinkWithLimit.

SP-008 P2-CONN-1 (2026-04-15): bounded-by-default. The previous NewMemorySink constructor installed no cap, which meant a badly-designed label (request ID, raw URL path, tenant ID — any high-cardinality drift value) would grow the series map without bound until the process OOM'd. The cap is chosen at 10_000 distinct series because (a) a well-designed surface in a single process rarely exceeds low thousands, (b) overflow is visibly signalled via OverflowDropped() and the throttled audit log so operators can see the problem before it becomes OOM, and (c) existing series continue to accept writes after the cap, so the metrics you already care about remain observable.

Variables

This section is empty.

Functions

func InterceptorLabelCacheOverflow added in v1.8.2

func InterceptorLabelCacheOverflow() uint64

InterceptorLabelCacheOverflow returns the cumulative count of getOrBuild* fall-throughs across all four interceptor label caches. Non-zero is a signal that somewhere, a `method` label is taking unbounded values and should be investigated — most commonly a client-side gRPC invoker that constructs FullMethod strings from user input. Safe to call from any goroutine; read-only.

func NewClientInterceptors

func NewClientInterceptors(sink Sink) (grpc.UnaryClientInterceptor, grpc.StreamClientInterceptor)

NewClientInterceptors returns a (unary, stream) interceptor pair that emits baseline RPC metrics for every outbound request. If sink is nil, NopSink is substituted.

Wire up via grpc.WithUnaryInterceptor / grpc.WithStreamInterceptor at Dial time, OR via the connector Client's interceptor slots if exposed.

func NewServerInterceptors

func NewServerInterceptors(sink Sink) (grpc.UnaryServerInterceptor, grpc.StreamServerInterceptor)

NewServerInterceptors returns a (unary, stream) interceptor pair that emits baseline RPC metrics for every served request. If sink is nil, NopSink is substituted so the call site can omit the nil check.

Wire up:

uIntr, sIntr := metrics.NewServerInterceptors(mySink)
svc.UnaryServerInterceptors  = append(svc.UnaryServerInterceptors,  uIntr)
svc.StreamServerInterceptors = append(svc.StreamServerInterceptors, sIntr)

Place these AFTER any context-decoration interceptors (auth, tenancy) and BEFORE recovery so panics still register as Internal errors in the metrics stream.

Types

type CounterSnapshot

type CounterSnapshot struct {
	Name   string
	Labels map[string]string
	Value  int64
}

CounterSnapshot is a point-in-time copy of one counter series.

type HistogramSnapshot

type HistogramSnapshot struct {
	Name   string
	Labels map[string]string
	Count  uint64
	Sum    float64
	Min    float64
	Max    float64
	Mean   float64
}

HistogramSnapshot is a point-in-time copy of one histogram series. Mean is provided for convenience (Sum / Count) when Count > 0.

type MemorySink

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

MemorySink is an in-process, dependency-free Sink implementation suitable for tests, single-instance services, and debug endpoints. It stores all counters and a small running summary (count, sum, min, max) for each histogram series.

MemorySink is NOT a substitute for a real metrics backend in multi-instance production deployments — there is no aggregation across processes. For production, implement Sink against your existing telemetry pipeline.

func NewMemorySink

func NewMemorySink() *MemorySink

NewMemorySink returns a MemorySink with a safe-by-default cardinality cap of DefaultMemorySinkLimit (10,000) distinct (name, labels) series. When a new series would exceed the cap, it is silently dropped and OverflowDropped() is incremented; existing series continue to accept writes.

SP-008 P2-CONN-1 (2026-04-15) — BREAKING CHANGE from v1.7.x: the pre-v1.8.0 NewMemorySink had no cap and could grow without bound under label-design mistakes. If you need the old unbounded behavior (e.g. for a test harness), call NewMemorySinkUnbounded. If you need a specific cap, call NewMemorySinkWithLimit. Callers with a well-designed metric surface (bounded label values) will not notice any behavioral difference at the default cap.

func NewMemorySinkUnbounded

func NewMemorySinkUnbounded() *MemorySink

NewMemorySinkUnbounded returns a MemorySink with NO cardinality cap. Every distinct (name, labels) pair allocates a new series and the sink will grow without bound until the process runs out of memory.

This constructor exists solely for callers who (a) fully trust their label-value surface (e.g. a unit test with a hard-coded set of series) OR (b) have an external circuit-breaker that will kill the process before cardinality becomes unsafe.

Prefer NewMemorySink (safe default) or NewMemorySinkWithLimit (explicit cap) unless you have a specific reason to want unbounded growth.

func NewMemorySinkWithLimit

func NewMemorySinkWithLimit(limit int) *MemorySink

NewMemorySinkWithLimit returns a MemorySink that caps the total number of distinct (name, labels) series across counters and histograms combined. When a new series would exceed limit, it is silently dropped and OverflowDropped() is incremented. A limit <= 0 is equivalent to NewMemorySinkUnbounded (no cap).

Existing series continue to accept writes even after the cap is reached — only the *creation* of new series is gated. This preserves continuity for the metrics you already care about while bounding the blast radius of a label-design mistake.

func (*MemorySink) Counter

func (m *MemorySink) Counter(name string, labels map[string]string, delta int64)

Counter increments the named counter by delta. Negative deltas are silently dropped to preserve monotonicity. If the sink was constructed with a cardinality cap and this call would create a new series that exceeds the cap, the call is dropped and OverflowDropped is incremented.

func (*MemorySink) Observe

func (m *MemorySink) Observe(name string, labels map[string]string, value float64)

Observe records a sample for the named histogram series. If the sink was constructed with a cardinality cap and this call would create a new series that exceeds the cap, the sample is dropped entirely and OverflowDropped is incremented — an unobservable series cannot have its min/max/sum/count updated.

SP-008 P1-CONN-3 (2026-04-15): fast path is now RLock-only. For an existing series, we take the read lock just long enough to look up the map entry, release it, and fold the sample into the histogramState via lock-free atomic CAS loops. The previous implementation held the write lock for the entire Observe call, which serialized EVERY histogram write across EVERY series in the process — a hot-path contention point that kicked in the moment two goroutines tried to observe two unrelated metrics at the same time. See the histogramState godoc for the atomic-field design and Snapshot-consistency trade-off.

func (*MemorySink) OverflowDropped

func (m *MemorySink) OverflowDropped() int64

OverflowDropped returns the cumulative count of Counter/Observe calls whose (name, labels) series was dropped because it would have exceeded the configured cardinality cap. Always 0 when the sink was constructed without a limit.

Scrape this alongside Snapshot() on a debug endpoint — a non-zero value means some label key has unbounded cardinality and the metric surface is incomplete. This is the user-visible signal that replaces the silent OOM mode of an uncapped sink.

func (*MemorySink) Snapshot

func (m *MemorySink) Snapshot() ([]CounterSnapshot, []HistogramSnapshot)

Snapshot returns a stable copy of the current counter and histogram state. The returned slices are sorted by series key for deterministic iteration order — useful for tests, dashboards, and snapshot diffing.

Snapshot is read-only with respect to MemorySink. Concurrent Counter / Observe calls are safe but may or may not be reflected in the returned snapshot depending on timing.

type NopSink

type NopSink struct{}

NopSink discards every metric event. Use it as a safe default when no real sink has been wired — every connector instrumentation site checks for nil and substitutes NopSink, so passing nil is also acceptable.

func (NopSink) Counter

func (NopSink) Counter(name string, labels map[string]string, delta int64)

Counter is a no-op for NopSink.

func (NopSink) Observe

func (NopSink) Observe(name string, labels map[string]string, value float64)

Observe is a no-op for NopSink.

type Sink

type Sink interface {
	Counter(name string, labels map[string]string, delta int64)
	Observe(name string, labels map[string]string, value float64)
}

Sink is the pluggable metrics backend interface. Every connector instrumentation point goes through this.

Counter increments a named, labeled counter by delta. delta MUST be non-negative; negative deltas are silently dropped by NopSink and MemorySink. (Prometheus-style counters are monotonic.)

Observe records a single sample for a named, labeled histogram. Implementations are free to bucket internally; the connector does not dictate a bucket layout.

Both methods MUST be safe for concurrent use across goroutines.

Implementations should treat a nil labels map as equivalent to an empty map (no labels). The provided implementations follow this convention.

Jump to

Keyboard shortcuts

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