Unreleased documentation. Choose your installed release in the version menu. Features described here may be absent from that release.
Controller and agent metrics¶
Use this reference to build queries and investigate configuration delivery. For scraping, dashboards, and alerts, start with monitoring setup. Scope queries to your installation when Prometheus monitors more than one release.
Reconciliation metrics¶
| Metric | Type | Description |
|---|---|---|
haptic_reconciliation_total |
Counter | Total reconciliation cycles triggered |
haptic_reconciliation_duration_seconds |
Histogram | Time spent in reconciliation cycles |
haptic_reconciliation_errors_total |
Counter | Failed reconciliation cycles |
haptic_render_profiles |
Gauge | Distinct backend profiles (defaults haptic-be-*) in the most recent render. Backends of the same shape collapse onto one profile, so this tracks the config's structural size independently of the raw backend count. Leader-only; 0 on followers |
haptic_render_warnings |
Gauge | Current template-recorded warnings, labeled by reason, from the last successful reconciliation. Only the leader reports them; resolved reasons disappear. Alert on sum by (reason) (haptic_render_warnings) > 0 to detect degraded routes such as ServicePortNotFound or BackendUnresolved |
haptic_render_total |
Counter | Reconcile renders by cache_state: cold re-evaluated every template, warm reused the incremental graph, replay reused the previous output unchanged. Counts on every replica, because followers render to keep their graph warm for a leadership change. A replica that keeps counting cold pays the full render cost on every change |
Key queries:
# Reconciliation rate per second
rate(haptic_reconciliation_total[5m])
# Average reconciliation duration
rate(haptic_reconciliation_duration_seconds_sum[5m]) /
rate(haptic_reconciliation_duration_seconds_count[5m])
# Success rate percentage
100 * (1 - (
rate(haptic_reconciliation_errors_total[5m]) /
rate(haptic_reconciliation_total[5m])
))
Deployment metrics¶
| Metric | Type | Description |
|---|---|---|
haptic_deployment_total |
Counter | Total deployment attempts |
haptic_deployment_duration_seconds |
Histogram | Time spent deploying to HAProxy |
haptic_deployment_errors_total |
Counter | Failed deployments |
haptic_haproxy_reloads_total |
Counter | HAProxy reloads triggered by deployments; compare with runtime apply counts when investigating reload frequency |
haptic_deploy_apply_total |
Counter | Applies an agent accepted, by pod and by the mode it reported: runtime, file_only, reload, scheduled or noop. The reload-free share of a rollout is the runtime+file_only+noop fraction |
haptic_apply_rejected_total |
Counter | Applies an agent refused or rolled back, by pod. Every increment carries HAProxy's own message in a Warning event and the pod's status condition |
haptic_agent_version_skew_total |
Counter | Applies degraded to full state plus a reload because the pod's agent speaks a different API major or doesn't execute an op kind. Can increase during a rolling upgrade; should stop increasing once versions match |
Key queries:
# Deployment rate
rate(haptic_deployment_total[5m])
# HAProxy reload rate — the capacity/SLO signal (a reload forks the process)
rate(haptic_haproxy_reloads_total[5m])
# Applies by what they did to the pod — `runtime` is the reload-free lane
sum by (mode) (rate(haptic_deploy_apply_total[5m]))
# Pods rejecting applies, worst first
topk(5, sum by (pod) (rate(haptic_apply_rejected_total[5m])))
# 95th percentile deployment latency
histogram_quantile(0.95, rate(haptic_deployment_duration_seconds_bucket[5m]))
# Deployment success rate
100 * (1 - (
rate(haptic_deployment_errors_total[5m]) /
rate(haptic_deployment_total[5m])
))
Fleet convergence & config staleness¶
Use fleet convergence to check whether a change reached every HAProxy pod. A failed attempt can recover on retry; persistent divergence means some pods still lack the desired configuration. These gauges report only on the leader.
| Metric | Type | Description |
|---|---|---|
haptic_haproxy_fleet_size |
Gauge | HAProxy pods the last deployment targeted |
haptic_haproxy_fleet_converged |
Gauge | HAProxy pods now at the desired config. Alert on haptic_haproxy_fleet_converged < haptic_haproxy_fleet_size |
haptic_last_full_sync_timestamp_seconds |
Gauge | Unix timestamp (seconds) of the last time the whole fleet converged. Seeded to controller start time, so before the first full sync staleness reads as uptime rather than the whole epoch. In steady state (no config or pod changes) it advances with the periodic drift-prevention deploy, so any staleness threshold you alert on must exceed spec.dataplane.driftPreventionInterval (default 60s) |
haptic_deployment_consecutive_failures |
Gauge | Consecutive deployments that didn't fully converge the fleet; resets to 0 on the first full sync |
Key queries:
# Pods not yet at the desired config right now (0 = fully converged)
haptic_haproxy_fleet_size - haptic_haproxy_fleet_converged
# How long since the whole fleet last converged (config staleness, seconds)
time() - haptic_last_full_sync_timestamp_seconds
# Deploys that failed to fully converge, back to back — alert on this instead of
# the error counter, now that transient deploy failures self-heal
haptic_deployment_consecutive_failures
The bundled HAProxyFleetDiverged alert detects pods that remain behind the desired configuration. If you add an alert on time() - haptic_last_full_sync_timestamp_seconds, set its threshold above your driftPreventionInterval to avoid alerting between scheduled checks.
Runtime operation metrics¶
The controller counts what it asked the fleet to run without a reload. A change the render couldn't express as a runtime op is a reload, and the reasons for that are in the pod's status.
| Metric | Type | Labels | Description |
|---|---|---|---|
haptic_runtime_backend_ops_total |
Counter | op |
Backend lifecycle operations the fleet applied at runtime, by op kind |
haptic_runtime_server_ops_total |
Counter | op |
Server lifecycle operations the fleet applied at runtime, by op kind |
haptic_runtime_backend_fallback_total |
Counter | reason |
Runtime backend batches a pod reloaded instead of running, by reason (name_collision: a fresh backend whose name a not-yet-deleted one still holds; op_rejected: any other refusal) |
haptic_runtime_map_divergence_total |
Counter | map |
Runtime maps whose post-apply read-back disagreed with the desired content, forcing a reload fallback. The map label names the file, so one map dominating the rate points at the template that builds it |
Key queries:
# Server changes applied without a reload
sum by (op) (rate(haptic_runtime_server_ops_total[5m]))
# Route adds and removes that stayed reload-free
sum by (op) (rate(haptic_runtime_backend_ops_total[5m]))
# Backend batches that fell back to a reload, by reason
sum by (reason) (rate(haptic_runtime_backend_fallback_total[5m]))
Agent metrics¶
Each agent serves its own /metrics on the pod's agent-metrics port, scraped
by the bundled PodMonitor. These are per-pod facts the controller can't see:
what the agent did with an apply after it accepted it.
| Metric | Type | Labels | Description |
|---|---|---|---|
haptic_agent_apply_total |
Counter | mode |
Applies this agent completed, by outcome mode |
haptic_agent_apply_rejected_total |
Counter | stage |
Applies this agent refused or rolled back, by the stage that failed |
haptic_agent_reloads_total |
Counter | result |
Reloads this agent asked the master process for |
haptic_agent_rollbacks_total |
Counter | — | Applies whose file set was restored to the last known good |
haptic_agent_op_errors_total |
Counter | kind |
Runtime ops HAProxy rejected, by op kind |
haptic_agent_invariant_violations_total |
Counter | name |
Invariants the agent observed failing. Any increment is a defect — alert on it |
haptic_agent_deferred_deletes_total |
Counter | kind, outcome |
Deferred runtime deletes, by object kind and whether they completed |
haptic_agent_generation |
Gauge | — | The agent's apply generation, which increases by one per successful apply |
haptic_agent_map_divergence_total |
Counter | — | Read-backs that found the running state different from the desired one; the controller counts the same events per map in haptic_runtime_map_divergence_total |
Key queries:
# Reload-free applies, fleet-wide
sum(rate(haptic_agent_apply_total{mode="runtime"}[5m]))
# Where applies are failing, by stage
sum by (stage) (rate(haptic_agent_apply_rejected_total[5m]))
# Any invariant violation at all
sum by (name) (increase(haptic_agent_invariant_violations_total[1h])) > 0
The controller and the agents count the same applies from either end: haptic_deploy_apply_total{pod,mode} on the controller, haptic_agent_apply_total{mode} on each pod. They agree in steady state; a difference is an apply one side never saw.
Where the old metrics went¶
| Removed | Replacement |
|---|---|
haptic_dataplane_api_operations_total |
haptic_deploy_apply_total{pod,mode} — applies, by what the pod did with them |
haptic_runtime_fast_path_fires_total |
haptic_deploy_apply_total{mode="runtime"} |
haptic_runtime_fast_path_applies_total |
haptic_runtime_server_ops_total |
haptic_runtime_fast_path_failures_total |
haptic_agent_op_errors_total{kind} on the pod, haptic_apply_rejected_total{pod} on the controller |
haptic_runtime_fast_path_server_updates_total |
haptic_runtime_server_ops_total{op} |
haptic_deploy_runtime_divergence_total |
haptic_runtime_map_divergence_total{map}, which now also covers what the agent reads back after its own ops |
Track HAProxy validation failures with
haptic_config_rejected_total{validator="haproxy"}.
Validation metrics¶
| Metric | Type | Description |
|---|---|---|
haptic_validation_total |
Counter | Total validation attempts |
haptic_validation_errors_total |
Counter | Failed validations |
Key queries:
# Validation rate
rate(haptic_validation_total[5m])
# Validation success rate
100 * (1 - (
rate(haptic_validation_errors_total[5m]) /
rate(haptic_validation_total[5m])
))
Resource metrics¶
| Metric | Type | Labels | Description |
|---|---|---|---|
haptic_resource_count |
Gauge | type |
Current count of watched resources |
haptic_haproxy_pods_rejected_total |
Counter | reason |
HAProxy pods refused admission by the discovery component. Persistent non-zero growth typically means the controller can't talk to the deployed HAProxy pods (for example, the bundled HAProxy major.minor differs from the chart's haproxyVersion). |
haptic_config_rejected_total |
Counter | validator |
Configuration refused by a validation gate. The validator label names which check rejected it: basic, template, jsonpath or validationtests for a HAProxyTemplateConfig load, coordinator when a validator timed out, and haproxy when the render gate's own haproxy -c refused a rendered config. Non-zero growth means the leader is refusing new config and continuing on the last-good one — alert on it: the operator's latest change isn't live. |
haptic_config_pinned |
Gauge | 1 while the render gate holds renders HAProxy refused twice in a row. The pods keep serving the last configuration HAProxy accepted, and nothing new reaches them until the input the ConfigValidated condition names is fixed. Leader-only; 0 on followers. |
|
haptic_component_mailbox_depth |
Gauge | component |
Events waiting in a component's coalescing mailbox. A sustained increase indicates that the component is falling behind. |
Key queries:
# All resource counts
haptic_resource_count
# Specific resource types
haptic_resource_count{type="ingresses"}
haptic_resource_count{type="services"}
haptic_resource_count{type="haproxy-pods"}
# Resource count changes
delta(haptic_resource_count[1h])
# Rejected HAProxy pods, broken down by reason
sum by (reason) (rate(haptic_haproxy_pods_rejected_total[5m]))
# Config rejected (leader refusing new config) — alert if > 0
sum by (validator) (rate(haptic_config_rejected_total[5m]))
# Renders held because HAProxy refused two in a row — alert if > 0
haptic_config_pinned
Event metrics¶
| Metric | Type | Description |
|---|---|---|
haptic_event_subscribers |
Gauge | Active event subscribers |
haptic_events_published_total |
Counter | Selected controller events; not a count of every internal event |
Key queries:
# Event publishing rate
rate(haptic_events_published_total[5m])
# Subscriber count (should be constant)
haptic_event_subscribers
# Subscriber changes (indicates component restarts)
delta(haptic_event_subscribers[5m])
Leader election metrics¶
| Metric | Type | Description |
|---|---|---|
haptic_leader_election_is_leader |
Gauge | 1 if this replica is leader, 0 otherwise |
haptic_leader_election_transitions_total |
Counter | Leadership transitions (gain/loss) |
haptic_leader_election_time_as_leader_seconds_total |
Counter | Cumulative time as leader |
Key queries:
# Current leader count (should be exactly 1)
sum by (namespace, job) (haptic_leader_election_is_leader)
# Identify leader pod
haptic_leader_election_is_leader == 1
# Leadership transition rate
rate(haptic_leader_election_transitions_total[1h])
# Average time as leader per transition
haptic_leader_election_time_as_leader_seconds_total /
haptic_leader_election_transitions_total
Scope these queries to one Helm release. The bundled ServiceMonitor uses a separate Service for each release. If you customize scraping, retain an equivalent release label when counting leaders.
Webhook metrics¶
Exposed when the validating admission webhook is enabled (controller.webhook.enabled=true).
| Metric | Type | Labels | Description |
|---|---|---|---|
haptic_webhook_requests_total |
Counter | gvk, result |
Total admission requests by GroupVersionKind and result |
haptic_webhook_request_duration_seconds |
Histogram | — | Time spent processing webhook requests |
haptic_webhook_validation_total |
Counter | gvk, result |
Validation outcomes per GVK. result is allowed, denied, or unregistered. An unregistered request is denied with status 503; growth of the fixed <unregistered> series means a webhook rule and the installed validators disagree. |
Key queries:
# Denial rate per resource kind
sum by (gvk) (rate(haptic_webhook_validation_total{result="denied"}[5m]))
# 95th percentile webhook latency (must stay well under the 10s admission timeout)
histogram_quantile(0.95, rate(haptic_webhook_request_duration_seconds_bucket[5m]))
Reconciliation queue¶
| Metric | Type | Description |
|---|---|---|
haptic_reconciliation_queue_wait_seconds |
Histogram | Time a triggered reconciliation waits in the coordinator queue before processing starts; rising values indicate the controller can't keep up with change volume |
Event bus backpressure¶
These complement haptic_events_published_total / haptic_event_subscribers from above.
| Metric | Type | Labels | Description |
|---|---|---|---|
haptic_events_dropped_total |
Counter | — | Critical event drops; equivalent to haptic_events_dropped_critical_total |
haptic_events_dropped_critical_total |
Counter | — | Critical event drops that can interrupt configuration delivery |
haptic_events_dropped_observability_total |
Gauge | — | Drops from observability-only subscribers (expected under load, non-alerting) |
haptic_events_dropped_by_subscriber_total |
Counter | subscriber, event_type |
Per-subscriber drop counts for diagnosing which component is falling behind |
Build info¶
| Metric | Type | Labels | Description |
|---|---|---|---|
haptic_build_info |
Gauge | version, haproxy_version, go_version |
Always 1; useful for joining build metadata into other queries |
# Attach the version from the same scraped controller
haptic_reconciliation_total * on(job, instance) group_left(version) haptic_build_info