Fluxheim metrics capture aggregate health and performance. Logs explain what happened for an individual event; metrics answer questions such as request rate, error rate, p95/p99 latency, cache efficiency, and upstream health.
The safe baseline is the existing Prometheus pull endpoint. Advanced per-vhost aggregation and remote push exporters are future optional add-ons.
- Keep
/metricspull mode available as the reliable baseline. - Avoid global locks on request worker hot paths.
- Avoid unbounded label cardinality from attacker-controlled input.
- Make remote push optional and non-blocking.
- Keep local metrics available even when remote exporters fail.
Planned feature split:
metrics = ["dep:prometheus"]
metrics-otlp = ["metrics", "dep:serde_json"]
metrics-advanced = ["metrics"]Reviewed optional crate candidates:
metrics 0.24.5, MIT: facade option.hdrhistogram 7.5.4, MIT/Apache-2.0: latency histogram option.opentelemetry-otlp 0.31.1, Apache-2.0: optional OTLP exporter.dashmap: latest observed release is7.0.0-rc2; avoid RC releases for MVP core. Re-evaluate a stable release if a concurrent map is needed.
Metrics must never create labels directly from arbitrary remote input.
Allowed labels:
- configured vhost name
- fixed request method bucket
- static status class
- known module name, for example
proxy,static,cache,admin - configured upstream name/address only when it came from config
- fixed legacy listener name
Forbidden labels:
- raw
Host - path
- query string
- user-agent
- referer
- client IP
- request ID
- arbitrary upstream response header values
Unknown or unsafe traffic must merge into fixed buckets:
unknowninvalid_hostlegacy_unidentifiedoverflow
This prevents Host-header and path-cardinality attacks from exhausting memory or making Prometheus unusable.
For each runtime snapshot, Fluxheim should prebuild metric buckets for configured vhosts. Request handling should resolve to a stable vhost index and update a bucket directly.
Per bucket:
- atomic request counters by status class
- atomic byte counters where byte counts are available
- atomic cache counters, or snapshot from cache activity counters
- latency buckets
Current cache baseline:
fluxheim_acme_events_total{event}fluxheim_php_requests_total{vhost,method,outcome,status_class}fluxheim_php_request_duration_seconds{vhost,method,outcome,status_class}fluxheim_php_stderr_events_total{vhost,state}fluxheim_php_fpm_retries_total{vhost,reason}fluxheim_php_fpm_pool_idle_connections{vhost,pool}fluxheim_php_fpm_pool_events_total{vhost,pool,event}fluxheim_cache_vhostsfluxheim_cache_enabled_vhostsfluxheim_cache_tiered_vhostsfluxheim_cache_configured_routesfluxheim_cache_policy_routesfluxheim_cache_enabled_routesfluxheim_cache_tiered_routesfluxheim_cache_memory_tiersfluxheim_cache_disk_tiersfluxheim_cache_lock_enabled_policiesfluxheim_cache_lock_wait_timeout_max_secondsfluxheim_cache_origin_protection_enabled_policiesfluxheim_cache_origin_protection_max_concurrent_fillsfluxheim_cache_peer_fill_enabled_policiesfluxheim_cache_peer_fill_peersfluxheim_cache_peer_fill_max_concurrent_requestsfluxheim_cache_activity_total{tier,event}fluxheim_cache_activity_scope_total{scope,vhost,route,tier,event}fluxheim_cache_operation_duration_seconds{scope,vhost,route,phase,operation}fluxheim_cache_purger_runs_total{outcome}fluxheim_cache_purger_entries_total{result}fluxheim_cache_purger_duration_seconds{outcome}fluxheim_edge_policy_events_total{scope,vhost,route,policy,outcome}fluxheim_load_balancer_events_total{scope,vhost,route,upstream,event}fluxheim_response_compressions_total{scope,vhost,route,encoding}fluxheim_stream_connections_total{route,outcome}fluxheim_stream_bytes_total{route,direction}
The configuration gauges are aggregate, label-free, and populated from
validated configuration when the metrics listener starts. The local
observability smoke verifies the route/vhost cache policy gauges, the
request-collapsing policy count, and the maximum configured lock wait timeout.
fluxheim_acme_events_total uses only bounded lifecycle labels: event is
pending, renewed, failed, reload_success, reload_failed,
reload_unavailable, tick_error, or other. It intentionally avoids domain
names, certificate paths, challenge URLs, and issuer secrets.
fluxheim_php_requests_total and fluxheim_php_request_duration_seconds use
configured vhost names plus bounded method, outcome, and status-class labels.
outcome is declined, redirect, forbidden, not_found, fpm_error,
connect_timeout, request_timeout, connection_error,
configuration_error, invalid_response, intercepted, offload,
offload_error, response, or other; these metrics avoid raw paths,
queries, script filenames, usernames, cookies, and FastCGI params.
fluxheim_stream_connections_total and fluxheim_stream_bytes_total use
configured stream route names only. Stream connection outcome is bounded to
completed, rejected, connect_error, timeout, shutdown, or error.
Stream byte direction is bounded to downstream_to_upstream,
upstream_to_downstream, or other.
fluxheim_php_stderr_events_total uses configured vhost names plus bounded
state labels of emitted, truncated, or other; it counts STDERR presence
without exposing PHP error text.
fluxheim_php_fpm_retries_total uses configured vhost names plus bounded
reason labels of connect_timeout, connection_error, or other; it counts
retry attempts without exposing upstream addresses, socket paths, raw request
paths, cookies, or FastCGI params.
fluxheim_php_fpm_pool_idle_connections and
fluxheim_php_fpm_pool_events_total use configured vhost and pool labels. The
pool label is default for a vhost-level PHP runtime or the configured route
name for a route-level PHP runtime. Pool event labels are bounded to connect,
reuse, return, drop_stale, discard_full, or other.
fluxheim_cache_activity_total uses only bounded labels: tier is memory,
disk, policy, or other, and event is hit, miss, store,
store_refusal, eviction, purge, pass, bypass, stale,
revalidate, peer_fill_hit, peer_fill_miss, peer_fill_error,
peer_fill_fallback, peer_fill_fail_closed, or other. These metrics
intentionally avoid raw hosts, paths, queries, cache keys, peer URLs, and purge
identities.
fluxheim_cache_activity_scope_total uses the same bounded tier and event
labels plus configured-name labels: scope is vhost or route, vhost is
the configured vhost name, and route is empty for vhost cache or the
configured route name for route cache. It intentionally avoids raw Host,
path, query, cache key, and purge identity labels. Cache operation duration
histograms use the same configured-name labels plus bounded phase and
operation labels. operation is lookup or lock_wait, so dashboards can
separate storage lookup cost from request-collapsing wait time without
high-cardinality request labels.
Cache purger duration histograms use the bounded purger outcome label only,
so operators can see when cleanup ticks are getting slow without exposing cache
paths, queries, cache keys, and purge identities. Future OpenTelemetry
attributes should mirror the same low-cardinality concepts used by admin JSON
and Prometheus.
Latency plan:
- Start with fixed histogram buckets implemented as atomics. This is easiest to bound and export.
- Evaluate
hdrhistogramlater with sharded/thread-local recorders and background aggregation. Avoid a single locked histogram on the request path.
Per vhost:
- request totals by status class
- latency histogram
- inbound/outbound bytes when available
- route module totals: static, proxy, cache, admin, future PHP/CGI, future legacy static
Cache:
- hits
- misses
- stores
- store refusals
- purges
- memory entries/bytes
- disk entries/bytes
Load balancer:
fluxheim_load_balancer_poolsreports configured load-balancer pool counts by boundedscope(vhostorroute) and bounded selection algorithm. It does not label configured vhost names, route names, or upstream addresses.fluxheim_load_balancer_events_totalcounts selected upstreams, retries, all-nodes-down/unavailable pools, queue wait/full/timeout outcomes, success/failure outcomes, and passive-health ejections with bounded configured vhost/route labels. Theupstreamlabel is empty unless the operator configures a safe low-cardinalityproxy.upstream_aliasesentry.fluxheim_load_balancer_queue_wait_secondsobserves bounded queue wait and timeout duration by configured vhost/route and bounded queue outcome. It does not label raw upstream addresses.- richer upstream health state transition metrics remain planned; they should not label raw upstream addresses unless an explicit low-cardinality alias is configured.
Admin/security:
- admin auth failures
- snapshot/reload/rollback actions
- self-healing confirms/rollbacks
- denied traversal attempts
- denied request-smuggling/legacy misuse attempts
Future PHP/CGI:
- runtime request totals
- runtime status/exit outcomes
- timeouts
- spawn/connect failures
- output limit violations
Baseline:
- Prometheus/OpenMetrics text through the local metrics listener.
- Loopback by default.
- Local smoke coverage starts Fluxheim with
profile-observability, sends a proxied request, verifiestraceparentpropagation, checks the local metrics listener for Fluxheim series, and optionally queries a Prometheus HTTP API. Usescripts/smoke_observability_local.sh; setFLUXHEIM_PROMETHEUS_REQUIRE_FLUXHEIM=1when a local Prometheus scrape job is expected to contain Fluxheim metrics, andFLUXHEIM_PROMETHEUS_REQUIRE_OTLP=1when Prometheus must have its OTLP metrics receiver enabled.
Optional push:
[metrics.otlp]uses the Fluxheim background task lifecycle when enabled, so exporter sleep/shutdown behavior follows the same runtime boundary as cache purging, ACME renewal, admin watchdog, and load-balancer refresh tasks.- future lifecycle work can add richer exporter health reporting.
- push failure must never block request workers.
- failed push keeps metrics locally available.
- do not buffer infinite historical metrics in memory.
Optional OTLP:
- behind
metrics-otlp. - default off.
- use explicit endpoint, interval, service name, and timeout config.
- can target a Prometheus HTTP OTLP metrics receiver at
/api/v1/otlp/v1/metricswhen Prometheus is started with--web.enable-otlp-receiver. - covers metrics only; traces still need an OpenTelemetry Collector or tracing backend.
- implemented now for counters, gauges, and histograms gathered from the existing Prometheus registry. Summaries remain a future exporter extension.
Exporter health metrics:
- implemented now for OTLP metrics exporter attempts:
fluxheim_metrics_otlp_exports_total{outcome}with boundedsuccess,failure, orotheroutcomes. - last successful push timestamp
- consecutive failures
- dropped export batches
- current circuit state
- reconnect attempts
Future HTTP/0.9 and headerless HTTP/1.0 traffic must not create vhost labels from
request data. Use legacy_unidentified or a configured legacy listener name.
Legacy metrics should be isolated from normal modern-protocol metrics so old devices cannot pollute dashboards or trigger cardinality growth.
Initial target:
[metrics]
enabled = true
listen = "127.0.0.1:9091"
require_loopback = true
# Optional native metrics bearer-token source.
# token_file = "/run/secrets/fluxheim-metrics-token"
[metrics.otlp]
enabled = false
endpoint = "http://127.0.0.1:9090/api/v1/otlp/v1/metrics"
service_name = "fluxheim"
interval_secs = 15
timeout_secs = 2
# tls_ca_cert_path = "/etc/fluxheim/otlp-ca.pem"
[metrics.advanced]
enabled = false
max_metric_vhosts = 10000
latency_buckets_ms = [1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000]
unknown_vhost_bucket = "unknown"
overflow_bucket = "overflow"
The OTLP metrics exporter accepts http:// and https:// endpoints. Use
loopback HTTP for same-host collectors and HTTPS for remote collectors.
metrics.otlp.tls_ca_cert_path can point at a PEM CA bundle for private PKI
collectors; otherwise the HTTPS client uses the bundled WebPKI roots. Plaintext
HTTP to a non-loopback collector logs a startup warning.
- Harden current labels: ensure request outcome labels are derived from
configured vhost names or fixed buckets only. Implemented for
fluxheim_proxy_requests_total: it uses configured vhost names, fixed request method buckets, fixed outcome classes, and fixed status classes instead of raw status codes. - Add vhost-indexed atomic counters.
- Add fixed atomic latency histograms.
- Add cache/load-balancer/admin/security counters.
- Add optional OTLP exporter. Implemented initially for counters/gauges over local OTLP/HTTP JSON.
- Add exporter health metrics.
- Add production-grade push/exporter lifecycle, retry, circuit breaker, and histogram support.
Required tests:
- Unknown Host maps to fixed
unknownbucket. - Missing Host and future HTTP/0.9 traffic map to
legacy_unidentifiedor a configured listener bucket. - Thousands of fake Host headers do not create new metric labels.
- Latency buckets export expected counts.
- Metrics update path does not require a global mutex.
- Push exporter failure does not block request handling.
- Exporter health metrics update on failure and recovery.