Skip to content

Spec: Prometheus metrics

What it does

iiiris optionally exports operational metrics in Prometheus exposition format at a GET /metrics endpoint. Metrics are in-process counters scraped per replica — no shared state, no external backend, no persistence; Prometheus aggregates across replicas via instance labels and handles counter resets natively. The subsystem is off by default and its absence leaves the request path entirely uninstrumented.

Surface

HTTP

  • GET <metrics.path> (default /metrics) — Prometheus exposition, served by promhttp over a dedicated registry.
  • With metrics.addr unset the route rides the main listener (peer to /health and /ready). With metrics.addr set (e.g. ":9090") the endpoint is served by a dedicated listener at that address and is not mounted on the main one. The dedicated listener shares the main server's read/write timeouts and shuts down with it.
  • With metrics.username and metrics.password both set, the endpoint requires HTTP Basic auth (constant-time comparison, WWW-Authenticate challenge on failure). Otherwise it is open.

Configuration

metrics:
  enabled: false      # master switch; everything below ignored when false
  path: /metrics      # route; empty falls back to /metrics
  addr: ""            # non-empty = dedicated listener instead of the main one
  username: ""        # both set = HTTP Basic required
  password: ""

Env overrides: IIIRIS_METRICS_ENABLED, IIIRIS_METRICS_PATH, IIIRIS_METRICS_ADDR, IIIRIS_METRICS_USER, IIIRIS_METRICS_PASS.

Go surface

  • internal/metrics: New(version) *Metrics (registry + all families), (*Metrics).Handler(user, pass) http.Handler, observer methods/factories (ObserveHTTP, CacheObserver, SourceObserver, ObservePipelineExecute, ObservePipelineQueueWait, ObserveAuthDecision), scrape-time collector registration (RegisterCacheFootprint, RegisterSourceHealth). Methods with a *Metrics receiver are nil-safe no-ops.
  • Observer hooks in instrumented packages (all plain funcs or structs of funcs; no metrics-library types): cache.OpObserver + cache.Observed{Render,Info,Origin} wrappers, source.OpenObserver + source.Observed wrapper (Unwrap- transparent), image.Observer via (*Pipeline).SetObserver, server.Deps.ObserveRequest / .MetricsHandler / .MetricsPath, and the pre-existing auth.RuleAuthorizer.Recorder.
  • cmd/iiirisd wires everything; no instrumentation is active when metrics.enabled is false.

Metric families

Namespace iiiris_. These names, types, and label sets are the wire contract.

Family Type Labels
iiiris_http_requests_total counter route, code (1xx5xx)
iiiris_http_request_duration_seconds histogram route
iiiris_http_response_bytes_total counter route
iiiris_cache_ops_total counter cache (render/info/origin/manifest), backend, op (hit/miss/put/delete/error)
iiiris_cache_entries, iiiris_cache_bytes gauge cache, backend
iiiris_source_open_total counter source, backend, outcome (ok/not_found/error)
iiiris_source_open_duration_seconds histogram source, backend
iiiris_pipeline_execute_duration_seconds histogram kind (thumbnail/reduced/full)
iiiris_pipeline_queue_wait_seconds histogram
iiiris_overlay_applied_total counter target (output/source), fit (none/shrink/scale/tile)
iiiris_overlay_composite_seconds histogram
iiiris_auth_decisions_total counter profile, outcome (allow/deny)
iiiris_source_healthy gauge (0/1) source
iiiris_build_info gauge (const 1) version, go_version
go_*, process_* client_golang default collectors

Contracts

  • Off by default. metrics.enabled: false in Default(); the zero-config path registers no route and wires no observers.
  • prometheus/client_golang has exactly one consumer: internal/metrics. Instrumented packages depend only on their own plain-func observer types.
  • Bounded label cardinality. route is a classifyRoute class, cache is one of four roles, backend/source/profile come from configuration. Never per-identifier, per-path, or per-client labels.
  • Single instrumentation point for HTTP. The Prometheus HTTP families and the admin dashboard's per-route stats are fed from the same countRequests middleware call, so the two surfaces cannot drift. Requests classifyRoute declines to track (the admin SSE stream) appear in neither.
  • Dedicated registry, never the client library's global default.
  • metrics.addr is exclusive: set, the endpoint exists only on the dedicated listener.
  • Basic auth requires both credentials. A half-configured pair leaves the endpoint open.
  • Pipeline timing covers successful renders only, labeled by the decode path actually taken; failures surface through the HTTP error metrics. Queue wait is reported separately and excluded from the execute duration.
  • Source open is measured at the upstream — inside any source.Cached wrapper — so it reflects true origin latency (time-to-response, not body transfer). Origin-cache effectiveness is iiiris_cache_ops_total{cache="origin"}.
  • Footprint gauges read cache.Stater at scrape time. Backends without Stater (S3) get no footprint gauge; a backend reporting bytes -1 (Redis) gets iiiris_cache_entries but no iiiris_cache_bytes. The filesystem backend's tree walk runs once per scrape.
  • iiiris_source_healthy runs the same bounded healthcheck as /ready once per scrape.
  • Observed cache wrappers do not forward Stater/Purger; the admin UI and the footprint collectors take the unwrapped caches.
  • Observer callbacks run on the request path and must stay cheap and non-blocking.
  • Histogram buckets are fixed (client default ~5 ms–10 s ladders; queue wait 1 ms–5 s), not operator-configurable.

Out of scope

  • OpenTelemetry SDK — dependency weight and abstraction unneeded for a metrics-only goal; OTel pipelines scrape this endpoint via the collector's Prometheus receiver. OTel tracing is a separate, opt-in subsystem (internal/telemetry; see telemetry.md) and does not touch this endpoint.
  • Push delivery (Pushgateway / remote-write) — pull is the standard for long-lived servers.
  • Per-identifier or per-path labels — unbounded cardinality; permanently out, not deferred.
  • Configurable histogram buckets — add only on demonstrated need.
  • Admin dashboard backed by Prometheus — the SSE dashboard keeps its own server.Stats (it needs on-demand quantile snapshots).
  • Metrics persistence across restarts — counter resets are native to the Prometheus model.