obs(spireentry): add Prometheus metrics and OTel tracing to entry reconciler [experimental] - #712
Draft
antoinedao-cohere wants to merge 6 commits into
Draft
Conversation
Add OTel SDK, OTLP/gRPC exporter, otelgrpc stats handler, and their
transitive dependencies (cenkalti/backoff, grpc-gateway, etc.) in
preparation for distributed tracing and richer gRPC instrumentation.
Bumps google.golang.org/genproto/googleapis/{api,rpc} to a newer patch
version pulled in by the OTel OTLP exporter.
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: antoinedao-cohere <antoine.dao@cohere.com>
Replace the single StaticEntryFailures placeholder with a full set of instruments covering the key observability signals identified during investigation: - ReconcileDuration: wall-clock duration of each full pass (kind/trigger/result) - ReconcilePhaseDuration: per-phase breakdown (list_entries, diff, create, …) - SPIREAPIRequestDuration: per-RPC latency (list, batch_create/update/delete) - EntryWriteTotal: per-operation, per-gRPC-code counters (ok/already_exists/…) - ReconcileTriggerTotal: enqueued vs. dropped trigger counts - EntryRenderCacheTotal: render cache hit/miss ratio - PodAttestationDelay: primary user-facing SLI (pod created → entry created) - ReconcileBatchEntries: batch size gauge for correlating size with latency Adds Register() to wire all metrics into a Prometheus Registerer, and ObservePhase() as a convenience stop-timer helper for reconcile phases. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: antoinedao-cohere <antoine.dao@cohere.com>
pkg/tracing provides Init() and Tracer() as the two surfaces used by the rest of the codebase: - Init() wires an OTLP/gRPC batch exporter when Enabled=true, or installs a no-op TracerProvider when Enabled=false (the default). A no-op provider means all otel.Tracer().Start() call sites compile and execute with zero allocation or span overhead. - Tracer() returns the global tracer scoped to the controller's instrumentation name, transparently delegating to whichever provider Init() installed. The TracingConfig struct is added to ControllerManagerConfigurationSpec so that the OTLP endpoint, sample ratio, and enabled flag can be set via the controller config file. The generated DeepCopy methods are updated accordingly. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: antoinedao-cohere <antoine.dao@cohere.com>
…eus metrics client.go: attach otelgrpc.NewClientHandler() as a gRPC stats handler so that every SPIRE API call automatically creates a child span under the active reconcile span. When tracing is disabled the handler is present but the no-op provider means no spans are allocated. entryapi.go: - ListEntries: time the full paginated read into SPIREAPIRequestDuration with the list_entries label. This directly measures the bottleneck phase identified in the investigation. - BatchCreateEntry / BatchUpdateEntry / BatchDeleteEntry: time each batch RPC and record per-result gRPC status codes into EntryWriteTotal. already_exists on create and not_found on delete quantify the wasted work that occurs when leader election is disabled and two replicas race. metrics_test.go: table-driven tests verifying counter increments for OK, AlreadyExists, and NotFound result codes on create/update/delete, and that SPIREAPIRequestDuration receives at least one observation after ListEntries. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: antoinedao-cohere <antoine.dao@cohere.com>
…ile loop reconciler.go (base reconciler): - Track whether each pass was triggered or periodic via lastTrigger, and record it as a label on ReconcileDuration and as a span attribute. - Count trigger events as enqueued vs. dropped in ReconcileTriggerTotal. A high drop rate signals that reconcile passes fill the entire GC interval and new pod events are being coalesced away. - Wrap each pass in a tracing.Tracer().Start() span; the no-op provider makes this zero-cost when tracing is disabled. spireentry/reconciler.go: - Wrap each named phase (list_entries, list_static_entries, list_cluster_spiffeids, build_node_map, render_state, diff, delete, create, update, status_update) in both an ObservePhase() stop-timer and a child OTel span. - Record batch sizes (delete/create/update) into ReconcileBatchEntries before issuing RPCs for correlation with SPIREAPIRequestDuration. - Instrument the render cache: increment EntryRenderCacheTotal with hit/miss on every renderPodEntry call. - Propagate pod.CreationTimestamp through AddDeclared → declaredEntry → createEntries so that PodAttestationDelay can be observed on codes.OK for genuinely new entries (AlreadyExists races are excluded). Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: antoinedao-cohere <antoine.dao@cohere.com>
…startup Call metrics.Register() alongside the existing StaticEntryFailures registration in init() so that all new histograms, counters, and gauges are visible in the /metrics endpoint from the first scrape. Call tracing.Init() early in run() before dialling the SPIRE server so that the OTLP/gRPC exporter connection is established before the first reconcile span is created. The shutdown function is deferred so the batch exporter is flushed cleanly on exit. When Tracing.Enabled is false (the default), Init() is a no-op — zero runtime overhead. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: antoinedao-cohere <antoine.dao@cohere.com>
antoinedao-cohere
force-pushed
the
controller-observability-improvements
branch
from
July 16, 2026 10:43
f3cd32e to
0535ebe
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
The default controller-runtime / kubebuilder metric tooling only instruments the Kubernetes reconcile loops (queue depth, processing latency, errors). It tells you nothing about what is happening inside a reconcile pass — specifically the SPIRE entry reconciler, which has its own internal loop that is not a standard controller-runtime reconciler.
We are seeing pod attestation delays of up to ~15 minutes in production. A pod is admitted to the cluster but its SPIRE registration entry is not created for a very long time, blocking workload identity issuance. We suspect the controller is the source of the delay but we have no visibility into which phase is slow. This change adds the instrumentation we need to confirm or rule that out.
What this adds
Prometheus metrics (
pkg/metrics)spire_controller_manager_reconcile_duration_secondskind,trigger,resultspire_controller_manager_reconcile_phase_duration_secondsphaselist_entries,list_static_entries,list_cluster_spiffeids,build_node_map,render_state,diff,delete,create,update,status_updatespire_controller_manager_spire_api_request_duration_secondsoperationlist_entries,batch_create,batch_update,batch_delete)spire_controller_manager_spire_entry_write_totaloperation,codeok,already_exists,not_found,error)spire_controller_manager_reconcile_trigger_totalresultenqueuedvsdropped— a high drop rate means new pod events are being coalesced awayspire_controller_manager_entry_render_cache_totalresulthitvsmissratiospire_controller_manager_pod_attestation_delay_secondspod.CreationTimestampto successful SPIRE entry creationspire_controller_manager_reconcile_batch_entriesoperationKey PromQL queries
Distributed tracing (
pkg/tracing)When
spec.tracing.enabled: trueis set in the controller config, an OTLP/gRPC trace exporter is initialised and every reconcile pass produces a tree of spans:When
enabled: false(the default), there is zero runtime overhead — a no-opTracerProvideris installed so all instrumentation call sites are compiled in but cost nothing.Design notes
cluster_static_entry_failurescounter).grpc.WithStatsHandler(otelgrpc.NewClientHandler())call instruments all gRPC calls automatically. When tracing is off, the handler is present but the no-op provider means no spans are created.pod_attestation_delay_secondsexcludesAlreadyExistsraces: if a second controller replica creates the same entry (because leader election is off), theAlreadyExistsresponse is not recorded as an attestation event — only genuine first-creates contribute to the SLI.already_exists/not_foundcounters:spire_entry_write_total{code="already_exists"}on creates andcode="not_found"on deletes are the fingerprint of two replicas running without leader election. Enabling leader election should drop these to zero.Checklist
EntryWriteTotalcounters (create/update/delete, OK + error codes)SPIREAPIRequestDurationobservation onListEntriesMade with Cursor