Skip to content

Observability Guide

This is the operator's guide to what VirtRigaud emits, what it means, and how to build dashboards and alerts from it. For the machine-readable catalog (exact help strings, label sets, histogram buckets) see the Metrics Reference. For circuit-breaker lifecycle details see Resilience. For migration recipes from v0.3.5 metrics see Upgrade Guide.

What VirtRigaud emits

The manager exposes all metrics on :8080/metrics (plain HTTP) by default. Pass --metrics-secure=true at manager startup to switch to HTTPS on the same port.

Three families of metrics live on that endpoint:

Family Who owns it Coverage
virtrigaud_* VirtRigaud — described in this guide 11 wired families + 1 deprecated in v0.3.6
workqueue_* controller-runtime standard metrics 9 families; per-reconciler queue depth, latency, retries. Present since v0.3.0.
go_* / process_* Go runtime Memory, GC pauses, goroutine count, file descriptors, etc.

The workqueue_* family is particularly useful alongside virtrigaud_manager_reconcile_*. See the kind → controller-name mapping in the Upgrade Guide if you are migrating from the deprecated virtrigaud_queue_depth gauge.


The virtrigaud_* catalog (v0.3.8)

1. virtrigaud_build_info

Type: Gauge
Labels: version, git_sha, go_version, component
Value: Always 1.

Populated by metrics.SetupMetrics() at manager startup. Use it to confirm that the running process matches your expected version + git SHA without looking at pod annotations.

# Is the right version running?
virtrigaud_build_info{component="manager", version="v0.3.8"}

# What git SHA is deployed?
virtrigaud_build_info{component="manager"} == 1

If this query returns nothing, the manager has not started successfully.


2. virtrigaud_manager_reconcile_total

Type: Counter
Labels: kind, outcome
outcome values: success, error, requeue

Incremented once per reconcile run by every reconciler (VirtualMachine, Provider, VMClass, VMImage, VMNetworkAttachment, VMAdoption, VMSnapshot, VMMigration, and — added in v0.3.8 — VMClone and VMSet). The outcome is inferred from the named return values of the reconcile function via a deferred timer block — no manual instrumentation at each return site.

New kind values in v0.3.8

v0.3.8 introduced the VMClone and VMSet controllers, so this counter now emits kind="VMClone" and kind="VMSet" series in addition to the existing kinds. The label set is otherwise unchanged. Example: virtrigaud_manager_reconcile_total{kind="VMClone",outcome="success"}.

requeue means the reconciler explicitly requested a requeue without returning an error (e.g. waiting for a provider task to complete). error means an error was returned and controller-runtime will apply exponential backoff. success means the reconcile completed cleanly.

# Reconcile error rate across all kinds
sum by (kind) (rate(virtrigaud_manager_reconcile_total{outcome="error"}[5m]))

# Per-kind success/error breakdown
sum by (kind, outcome) (rate(virtrigaud_manager_reconcile_total[5m]))

Alerting: A sustained non-zero rate on outcome="error" for a specific kind suggests a bug or misconfiguration in that reconciler's path.


3. virtrigaud_manager_reconcile_duration_seconds

Type: Histogram
Labels: kind
Buckets: Exponential, 1ms to ~32s (15 buckets)

Wall-clock time per reconcile run. Recorded alongside virtrigaud_manager_reconcile_total.

# 95th-percentile reconcile time for VirtualMachine
histogram_quantile(0.95,
  sum(rate(virtrigaud_manager_reconcile_duration_seconds_bucket{kind="VirtualMachine"}[5m]))
  by (le)
)

Reconciles that consistently run longer than ~5s suggest slow provider RPCs or stuck task polling. Correlate with virtrigaud_provider_rpc_latency_seconds.


4. virtrigaud_errors_total

Type: Counter
Labels: reason, component

A typed error counter used to slice failures by root cause without scraping logs. Each reconciler defines a small set of errReason* constants (e.g. get-vm, deps-not-found, provider-resolve, provider-validate, provider-describe, provider-task-status, provider-delete). The component label is always manager for errors emitted from reconcilers.

# Top-5 error reasons in the last 10 minutes
topk(5, sum by (reason) (rate(virtrigaud_errors_total{component="manager"}[10m])))

# Are any VMs failing because their Provider CR can't be resolved?
rate(virtrigaud_errors_total{reason="provider-resolve"}[5m])

Use this before opening the logs to triage whether a surge of errors is infrastructure (provider-describe, provider-task-status) or configuration (deps-not-found, provider-resolve).


5. virtrigaud_provider_rpc_requests_total

Type: Counter
Labels: provider_type, method, code
Wired since: v0.3.5 (G4 / PR #107)

Every outbound gRPC RPC from the manager to a provider pod is counted here. method is the short RPC name (e.g. Create, Delete, Power, Describe, TaskStatus, SnapshotCreate). code is the canonical gRPC status code string from codes.Code.String()OK, Unavailable, DeadlineExceeded, NotFound, etc.

Circuit-breaker rejections are visible here

The metrics interceptor runs before the circuit-breaker interceptor in the chain. Breaker fast-fails appear as code="Unavailable" — not as silent drops. If virtrigaud_circuit_breaker_state is 2 (Open) and virtrigaud_provider_rpc_requests_total{code="Unavailable"} is climbing, that is the breaker protecting the provider, not the provider itself refusing RPCs.

# Per-provider gRPC error rate
sum by (provider_type, code) (
  rate(virtrigaud_provider_rpc_requests_total{code!="OK"}[5m])
)

# Fraction of Describe RPCs that time out
rate(virtrigaud_provider_rpc_requests_total{method="Describe",code="DeadlineExceeded"}[5m])
  /
rate(virtrigaud_provider_rpc_requests_total{method="Describe"}[5m])

6. virtrigaud_provider_rpc_latency_seconds

Type: Histogram
Labels: provider_type, method
Buckets: Exponential, 1ms to ~32s (15 buckets)
Wired since: v0.3.5 (G4 / PR #107)

Wall-clock time for each outbound RPC, recorded by the same metrics interceptor as virtrigaud_provider_rpc_requests_total. Useful for detecting a degraded-but-not-failing provider (latency rising, error rate still low).

# 99th-percentile Create latency per provider type
histogram_quantile(0.99,
  sum(rate(virtrigaud_provider_rpc_latency_seconds_bucket{method="Create"}[5m]))
  by (le, provider_type)
)

7. virtrigaud_circuit_breaker_state

Type: Gauge
Labels: provider_type, provider
Values: 0 = Closed, 1 = HalfOpen, 2 = Open
Added: v0.3.6 (G6 / PR #111)

One time-series per Provider CR. The gauge is seeded to 0 (Closed) at the moment the manager constructs the gRPC client for a Provider, so all Provider rows appear on /metrics from boot — operators get a stable label set to dashboard against even before any failures occur.

For the full circuit-breaker lifecycle (state machine diagram, failure classification, HalfOpen trial semantics) see Resilience.

# Alert: any breaker non-closed for more than 5 minutes
ALERT ProviderCircuitBreakerOpen
  IF virtrigaud_circuit_breaker_state > 0
  FOR 5m
  LABELS { severity = "warning" }
  ANNOTATIONS {
    summary = "Provider {{ $labels.provider }} circuit breaker is {{ $value | humanize }} (0=Closed,1=HalfOpen,2=Open)"
  }

v0.3.6-rc1 smoke observation

On the vr1.lab.k8 lab cluster, the libvirt breaker tripped immediately on first deploy because of a persistent libvirt-SSH connectivity issue (issue #I1). This is correct behaviour — the breaker is working as designed, and virtrigaud_circuit_breaker_state{provider="libvirt-lab"} 2 is the signal that prompts you to check the SSH tunnel rather than grep manager logs. See Resilience for the full narrative.


8. virtrigaud_circuit_breaker_failures_total

Type: Counter
Labels: provider_type, provider
Added: v0.3.6 (G6 / PR #111)

Incremented on every infra-class RPC failure (Unavailable, DeadlineExceeded, Internal, Unknown) counted against the breaker. Business errors (NotFound, InvalidArgument, etc.) do not increment this counter — those indicate the provider is healthy and the request was invalid.

The counter increments every time recordFailure() is called inside the CircuitBreaker, not just on transitions. This means it answers "how often is this provider failing at the infrastructure level?" independently of whether the breaker has tripped.

# Rate of infra failures per provider
rate(virtrigaud_circuit_breaker_failures_total[5m]) by (provider_type, provider)

9. virtrigaud_vm_operations_total

Type: Counter
Labels: operation, provider_type, provider, outcome
operation values: Create, Delete, Power, Describe, Reconfigure
outcome values: success, error
Added: v0.3.6 (G7.1 / PR #124)

Per-Provider VM operation outcome counter. Recorded via defer c.recordVMOp(op, &retErr) at the top of each VM-operation method in the gRPC client. The outcome is inferred from the named return value at function exit — same pattern as the reconcile-level metrics.

This gives a higher-level view than virtrigaud_provider_rpc_requests_total: one Create operation may involve several RPCs (Create + repeated TaskStatus polls), but it produces exactly one sample here at the end.

# Per-Provider VM-op error rate
sum by (provider) (rate(virtrigaud_vm_operations_total{outcome="error"}[5m]))

# Create success rate for vsphere provider
rate(virtrigaud_vm_operations_total{operation="Create",provider_type="vsphere",outcome="success"}[5m])
  /
rate(virtrigaud_vm_operations_total{operation="Create",provider_type="vsphere"}[5m])

Alerting recipe:

ALERT VMOperationsErrorRateHigh
  IF sum by (provider) (rate(virtrigaud_vm_operations_total{outcome="error"}[5m]))
       /
     sum by (provider) (rate(virtrigaud_vm_operations_total[5m])) > 0.1
  FOR 10m
  LABELS { severity = "warning" }

10. virtrigaud_ip_discovery_duration_seconds

Type: Histogram
Labels: provider_type
Buckets: Exponential, 100ms to ~100s (10 buckets)
Added: v0.3.6 (G7.2 / PR #127)

Measures the wall-clock time from kubectl apply (specifically, vm.CreationTimestamp) to the moment the first IP address appears in vm.Status.IPs. This is the operator-visible SLO answer to "how long from create to first IP on this provider type?"

A single sample is emitted per VM on the no-IPs → has-IPs transition. The gate in recordIPDiscoveryIfFirstSeen requires: the current vm.Status.IPs is empty, the provider just returned at least one IP, and vm.CreationTimestamp is non-zero. Because vm.Status.IPs is persisted to etcd as soon as the gate fires, the metric is idempotent across manager restarts — a restart during the same VM's lifecycle will not emit a second sample.

# 95th-percentile IP discovery time for libvirt
histogram_quantile(0.95,
  sum(rate(virtrigaud_ip_discovery_duration_seconds_bucket{provider_type="libvirt"}[5m]))
  by (le)
)

# Median across all provider types
histogram_quantile(0.50,
  sum(rate(virtrigaud_ip_discovery_duration_seconds_bucket[5m]))
  by (le, provider_type)
)

Adopted VMs

VMs created by the VMAdoption controller (watching virtrigaud.io/adopt-vms: "true" on Provider CRs) already have IPs at the moment their VirtualMachine CR is created. For those VMs the gate fires immediately and the duration will be very short (~milliseconds). Filter by provider_type if you want to compare native-create latency vs adoption latency.


11. virtrigaud_provider_tasks_inflight

Type: Gauge
Labels: provider_type, provider
Added: v0.3.6 (G7.3 / PR #129)

Tracks how many async tasks the manager is currently monitoring for a given Provider CR. The gauge is seeded to 0 at startup (so all Provider rows appear on /metrics immediately — see note on build_info above).

trackTaskStart is called in the 9 task-creating RPCs: Create, Delete, Power, Reconfigure, SnapshotCreate, SnapshotDelete, SnapshotRevert, ExportDisk, ImportDisk. trackTaskDone is called in IsTaskComplete and TaskStatus when the terminal state (Done or Error) is observed.

The gauge measures "tasks THIS manager instance is tracking." After a manager restart, the inflightTasks map starts empty, so the gauge resets to 0 even if the provider has tasks running from the previous instance. The reconciler will discover those tasks from vm.Status.LastTaskRef and repopulate the set on the next reconcile cycle.

# Is any Provider accumulating tasks faster than it's completing them?
virtrigaud_provider_tasks_inflight > 5

# Task inflight count across all providers
sum by (provider) (virtrigaud_provider_tasks_inflight)

Alerting recipe:

ALERT ProviderTasksAccumulating
  IF virtrigaud_provider_tasks_inflight > 5
  FOR 5m
  LABELS { severity = "warning" }
  ANNOTATIONS {
    summary = "Provider {{ $labels.provider }} has {{ $value }} inflight tasks — possible task polling loop or provider wedge"
  }

Deprecated: virtrigaud_queue_depth

Deprecated in: v0.3.6 (G7.4 / PR #131)
Removal: v0.4.0 or later
Help string: [DEPRECATED v0.3.6 — use controller-runtime's workqueue_depth{name} instead. See CHANGELOG.] Current depth of work queue by kind.

This gauge is redundant with controller-runtime's standard workqueue_depth{name} metric, which has been present on /metrics since v0.3.0. Reinventing it under a virtrigaud_* namespace forces operators to dashboard two redundant series.

The variable and helper remain callable for compile compatibility with out-of-tree code. Production code in this repo no longer calls SetQueueDepth.

Migration: replace virtrigaud_queue_depth{kind=<KIND>} queries with workqueue_depth{name=<controller-name>}. The full kind → controller-name mapping table lives in the Upgrade Guide.


Wiring summary

The following table shows which code path populates each metric and when:

Metric Code path Fires when
build_info metrics.SetupMetrics() in cmd/manager/main.go Manager start
reconcile_total + reconcile_duration Deferred ReconcileTimer.Finish() in each Reconcile() function Every reconcile
errors_total metrics.RecordError() at each error site in controllers Each error return
rpc_requests_total + rpc_latency providerRPCMetricsInterceptor on the gRPC client chain Every outbound RPC
circuit_breaker_state CircuitBreaker.transitionTo*() CB state change + at client construction (seed)
circuit_breaker_failures_total CircuitBreaker.recordFailure() Every infra-class RPC failure
vm_operations_total defer c.recordVMOp(op, &retErr) in gRPC client methods Each VM operation (Create/Delete/Power/Describe/Reconfigure)
ip_discovery_duration recordIPDiscoveryIfFirstSeen() in VirtualMachine controller First no-IPs → has-IPs transition per VM
provider_tasks_inflight trackTaskStart / trackTaskDone in gRPC client Each task-creating or task-completing RPC

Scraping setup

ServiceMonitor (Prometheus Operator)

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: virtrigaud-manager
  namespace: virtrigaud-system
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: virtrigaud
      app.kubernetes.io/component: manager
  endpoints:
  - port: metrics
    interval: 30s
    path: /metrics

Verify the endpoint manually

kubectl port-forward -n virtrigaud-system svc/virtrigaud-manager 8080:8080
curl -s http://localhost:8080/metrics | grep '^virtrigaud_build_info'
# Expected: virtrigaud_build_info{component="manager",git_sha="...",go_version="...",version="v0.3.8"} 1

Troubleshooting

virtrigaud_build_info absent

The manager pod has not started successfully. Check kubectl get pods -n virtrigaud-system and kubectl logs.

virtrigaud_circuit_breaker_state absent

The manager is running but no Provider CRs have been created yet, or the manager version predates v0.3.6. Check kubectl get providers -A.

virtrigaud_ip_discovery_duration_seconds has no samples

No VM has completed the no-IPs → has-IPs transition since the last manager restart. This is normal on a freshly deployed cluster with no VMs, or if all current VMs were adopted (they arrive with IPs and the gate fires near-instantly).

High virtrigaud_errors_total{reason="provider-resolve"}

The VirtualMachine controller cannot find the Provider CR referenced in spec.providerRef. Verify the Provider CR exists in the correct namespace and the providerRef fields match exactly.

virtrigaud_circuit_breaker_state{provider="X"} 2 on startup

The provider is unreachable. Ten infra-class failures are required to trip the breaker from Closed, so this indicates at least 10 failed RPCs occurred before you checked — usually because the provider pod is not running or SSH connectivity is broken. See Resilience for the recovery path.