Skip to content

feat(observability): measure gateway provision duration - #243

Merged
jsell-rh merged 3 commits into
mainfrom
feat/gateway-provision-duration-metric
Sep 3, 2026
Merged

feat(observability): measure gateway provision duration#243
jsell-rh merged 3 commits into
mainfrom
feat/gateway-provision-duration-metric

Conversation

@jsell-rh

@jsell-rh jsell-rh commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add the gateway.provision.duration OTLP histogram.
  • Measure from the API server created_at value to the successful Running update time.
  • Record direct provisioning and delayed Provisioning to Running completion.
  • Exclude later Degraded to Running recovery events and Gateway identifiers.
  • Add explicit buckets from 1 second through 15 minutes.
  • Add tests and update the observability documentation.

Validation

  • go test ./... in components/control-plane
  • make lint-control-plane
  • make check

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Team

Run ID: 361e94cc-d98b-411c-9195-96098a7075d1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@jsell-rh

jsell-rh commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

Amber review: comment

Amber review

Status: Complete

View the submitted review.

@jsell-rh jsell-rh left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

COMMENT — This is a clean, well-scoped observability change: the new gateway.provision.duration histogram, its one-observation semantics, the spec, and the tests are consistent and correct as written against the current code. I have no blocking or critical findings on the PR itself; the notes below are hardening suggestions plus cross-PR coordination the maintainers should sequence.

Amber Analysis

The histogram is recorded on both the direct (Handle) and delayed (reconcileGatewayHealth) paths, timestamps come from the API server's created_at/updated_at (avoiding control-plane clock skew), and invalid/reversed/missing timestamps are safely ignored so telemetry cannot alter reconciliation behavior. The single-observation guarantee is real today, but it is split across two mechanisms with asymmetric guards, which is the main thing to watch.

Minor

  1. Initial-reconcile record path has no explicit "first Running" guard. In reconciler.go (~L1533-L1545) the metric is recorded whenever updateGatewayHealth(...,"Running",...) succeeds, with no equivalent of the health path's isGatewayProvisionCompletion check. Its correctness is entirely load-bearing on the top-level phase gate (~L1360) returning early for Running/Provisioning/Degraded. That holds now, but the coupling is implicit and fragile — if the gate ever admits an already-Running gateway back into this block, the histogram will record a second, inflated created_atupdated_at observation. Consider recording only when the prior phase was not Running (symmetry with the health path), which also self-documents the invariant. Confidence: High.

  2. New phase string literals reintroduce magic values. metrics.go (isGatewayProvisionCompletion, L48) and the "Running"/"Healthy" literals added in reconciler.go/health.go duplicate phase strings that already appear across the control plane. A shared constant would prevent drift. Confidence: Medium.

Cross-PR coordination

Two open pull requests touch the assumptions this change depends on and need a maintainer decision or a merge ordering:

  • #151 (gate gateway re-provisioning on desired-state convergence) — This PR's single-observation guarantee for the direct path depends on the current phase gate in GatewayReconciler.Handle blocking any already-Running gateway from re-entering the full provision block. #151 re-keys that gate on convergence (observed_generation == generation) so that a desired-spec change to a Running gateway falls through and re-applies "regardless of phase." Under #151, a spec change would drive a Running gateway back to Running through the unguarded record path here and emit a second, inflated gateway.provision.duration sample, violating the spec's "first successful transition to Running" rule. Decide the ordering: if #151 lands, this PR's direct-path record must gain an explicit first-Running guard.

  • #239 (standardize gateway health and readiness vocabulary)#239 establishes a single-source-of-truth gatewayhealth phase vocabulary and explicitly removes the magic phase literals from the same reconciler.go/health.go functions this PR edits, while this PR adds new literals (isGatewayProvisionCompletion, "Running"/"Healthy"). Whichever merges second must adopt the shared constants; the two should be reconciled so the standardization effort isn't immediately re-diluted.


Findings Summary (ordered by severity, highest first):

  1. [Minor] Direct-path metric record relies implicitly on the phase gate with no explicit first-Running guard - Robustness (reconciler.go L1533-L1545)
  2. [Minor] New phase string literals duplicate existing magic values instead of a shared constant - Consistency (metrics.go L48)

Convention Checklist:

Convention Result
No panic() in production code Pass
Errors wrapped with fmt.Errorf context Pass
No secrets in logs or responses Pass
Reconcile pattern (not create-or-skip) Pass
Proper context propagation (no context.TODO()) Pass
Telemetry cannot alter reconciliation behavior Pass
Test Diff Scrutiny (no flipped pre-existing assertions) Pass
Spec/docs updated with the change Pass
Conventional commit message Pass

r.updateGatewayHealth(ctx, event.ResourceID, "Running", "Healthy")
// The phase gate prevents a second full provision after Running.
if runningGateway := r.updateGatewayHealth(ctx, event.ResourceID, "Running", "Healthy"); runningGateway != nil {
observeGatewayProvisionDuration(ctx, runningGateway)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The direct-path record fires whenever the update to Running succeeds, with no equivalent of the health path's isGatewayProvisionCompletion first-Running check. Its single-observation correctness is load-bearing on the top-level phase gate (~L1360) returning early for already-Running/Provisioning/Degraded gateways. That is correct today, but the coupling is implicit: if that gate is ever changed to admit an already-Running gateway back into this block, this would emit a second, inflated created_at->updated_at sample. Consider recording only when the prior phase was not Running, mirroring the health path and self-documenting the invariant.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in b97a99d. The handler now uses the stored phase before it enters the phase gate or writes Provisioning. A retry also carries its phase before the retry adapter clears it. Work that started in Running or Degraded suppresses the observation. Thus, a future convergence gate cannot emit a second sample after a restart. A concurrent claim also coordinates the direct and health paths.

// gateway can stay in Provisioning after the first reconcile while its route
// becomes ready. A Running gateway that fails moves through Degraded instead.
func isGatewayProvisionCompletion(currentPhase, desiredPhase string) bool {
return currentPhase == "Provisioning" && desiredPhase == "Running"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These phase strings ("Provisioning", "Running"), along with the "Running"/"Healthy" literals added in reconciler.go/health.go, duplicate phase values that already appear as magic literals across the control plane. A shared phase-vocabulary constant would prevent drift between the completion check and the values actually written by updateGatewayHealth.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in b97a99d. Shared Gateway phase and healthy-status constants now live in gateway_vocabulary.go. The reconciler, health loop, and metric predicates use these constants. When PR 239 and this branch are combined, its cross-component gatewayhealth package can replace these local constants.

@jsell-rh

jsell-rh commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

Amber review: comment

Amber review

Status: Complete

View the submitted review.

@jsell-rh jsell-rh left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

This change adds a well-scoped gateway.provision.duration OTLP histogram with careful coordination between the event-driven and health reconcile paths, sound timestamp validation, and thorough table-driven tests. The control-plane code is correct and merge-ready on its own; the one item that needs human attention is a cross-PR design decision about how the gateway phase vocabulary is standardized (see Cross-PR coordination).

Amber Assessment

Confidence: High (90%) on the correctness review; Medium on the cross-PR interaction pending a maintainer decision.

Strengths worth calling out:

  • The one-observation-per-Gateway rule is enforced with a single process-wide sync.Map claim, and both promotion paths funnel through it, so the event-driven and health paths cannot double-count the first Running transition.
  • gatewayProvisionDuration correctly rejects missing/invalid/reversed timestamps (CheckValid, negative duration), so telemetry can never alter reconciliation behavior.
  • PhaseBeforeRetry is threaded through the watcher so a forced-recovery retry (which clears the phase to bypass the gate) still lets the reconciler distinguish a Degraded/Running recovery from a genuine first provision. The proto.Clone copy avoids mutating the shared latest entry.
  • The metric carries no Gateway identifier attribute, and the test explicitly asserts point.Attributes.Len() == 0 plus the exact bucket bounds and unit. Spec, README, and RECONCILE.md are updated consistently.

Minor observations:

  • observedGatewayProvisions entries are only removed on EventDeleted. For every Gateway that reaches (or is seeded in) Running/Degraded, a claim entry persists for the life of the Gateway. This is bounded by the live Gateway count and acceptable, but if a delete event is ever missed the entry lingers. A periodic reconcile against the known Gateway set (or a TTL) would make cleanup self-healing. Not blocking.
  • RecordGatewayProvisionDuration guards duration < 0 even though gatewayProvisionDuration already filters negatives upstream. Harmless defensive redundancy.

No panic(), error paths log and return without swallowing failures, no secrets in logs, and the histogram is registered with an explicit error return. SecurityContext / RBAC / OpenAPI surfaces are untouched.

Cross-PR coordination

Another open pull request, #239, standardizes the exact same Gateway phase/status vocabulary that this PR touches, but with a different design and ownership boundary. This PR introduces a control-plane-local gateway_vocabulary.go (gatewayPhase*, gatewayStatusHealthy) and rewrites the magic-string phase literals in internal/reconciler/health.go, internal/reconciler/reconciler.go, and internal/watcher/watcher.go to use it. #239 replaces those same literals in those same files, but sources the constants from a new shared cross-component package (components/api-server/pkg/gatewayhealth) imported by both the API server and the control plane, and additionally adds API-server-side phase validation. These are two competing sources of truth for the same vocabulary.

Maintainers should decide which vocabulary source the control plane adopts, and merge in a defined order: if #239 lands first, this PR should drop gateway_vocabulary.go and consume gatewayhealth constants (the shared single source of truth); if this PR lands first, #239's owner should reconcile the control-plane edits against these local constants. Merging both as-is yields duplicated constant definitions and conflicting edits to the same phase literals. This is a design/ownership decision, not a mechanical merge conflict.

Findings Summary (ordered by severity, highest first)

  1. [Minor] observedGatewayProvisions claim entries are removed only on delete events; a missed delete leaks one entry. Consider a periodic reconcile against the known Gateway set or a TTL - Observability / Resource lifecycle (metrics.go L16, L41-42)
  2. [Minor] Redundant duration < 0 guard in RecordGatewayProvisionDuration (already filtered upstream) - Style (otel/metrics.go)

Convention Checklist

Convention Result
No panic() in production code Pass
Errors wrapped / not swallowed on error paths Pass
No secrets in logs or responses Pass
Reconcile pattern (not create-or-skip) Pass
Status updated on error paths Pass
Context propagated (no context.TODO()) Pass
Test Diff Scrutiny (existing assertions) Pass (only additive tests; existing seed_test.go change adds a new test)
Spec / docs consistent with code Pass
Conventional commit messages Pass

Comment thread components/control-plane/internal/reconciler/metrics.go
Comment thread components/control-plane/internal/otel/metrics.go
@jsell-rh
jsell-rh added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit b3dd1e8 Sep 3, 2026
17 checks passed
@jsell-rh
jsell-rh deleted the feat/gateway-provision-duration-metric branch September 3, 2026 21:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant