Skip to content

Expose the metrics migration switch to CLI and operator - #6371

Merged
amirejaz merged 7 commits into
mainfrom
metrics-transport-port-optin
Aug 26, 2026
Merged

Expose the metrics migration switch to CLI and operator#6371
amirejaz merged 7 commits into
mainfrom
metrics-transport-port-optin

Conversation

@amirejaz

Copy link
Copy Markdown
Contributor

Summary

#6370 added metricsOnTransportPort, but left it reachable only by editing stored
configuration — so nobody could actually use the migration window it exists for.
This exposes it:

  • CLI: --otel-metrics-on-transport-port
  • Operator: prometheus.metricsOnTransportPort on MCPTelemetryConfig

That makes the intended migration possible: point a scraper at the diagnostics port,
confirm metrics arrive, then turn the transport-port copy off to prove nothing else
depended on the old location — all before the default flips.

Stacked on #6370, which introduces the field. Review that first; this diff is
against it and will be retargeted to main once it merges.

Part of #6271

Type of change

  • Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Dependency update
  • Documentation
  • Other (describe):

Test plan

  • Unit tests (task test)
  • E2E tests (task test-e2e)
  • Linting (task lint-fix)

TestResolveMetricsOnTransportPort covers all three CLI states — absent, explicitly
true, explicitly false — driving a real cobra command rather than asserting on the
resolver in isolation, since the behaviour depends on Flags().Changed.

Pre-existing failures on main in untouched files: TestValidateOCIRegistryHost,
TestParseGitReference_*, and lint findings in cmd/thv/app/upgrade.go and
pkg/vmcp/config/crd_cli_roundtrip_test.go.

Does this introduce a user-facing change?

Yes, additive: a new CLI flag and a new optional CRD field. Neither changes behaviour
unless set — the default is unchanged.

Special notes for reviewers

Preserving the tri-state is the whole point, and both surfaces could have broken it.

  • The CLI flag is bound as a plain bool (cobra cannot bind *bool) and read
    through resolveMetricsOnTransportPort, which consults Flags().Changed so an
    absent flag stays unset instead of resolving to false.
  • The CRD field is an optional pointer with no +kubebuilder:default, so the
    API server materialises nothing at admission.

Either shortcut would pin the window's value into every workload created during it,
and the eventual cutover would move nobody while appearing to work.

Drift table: metricsOnTransportPort now exists on both sides, so it moves out of
telemetryIgnoredOnRuntimeOnly and into telemetryFieldMappings.

Builder option instead of another parameter. WithMetricsOnTransportPort is a
dedicated option rather than an 11th positional argument on
WithTelemetryConfigFromFlags/MaybeMakeConfig. The field is transitional, and
deleting it later should touch one function rather than two long signatures. It must
be applied after the telemetry config, which the call site does and the doc comment
states.

Remaining before the window can start: nothing on the opt-in side. After it: flip
DefaultMetricsOnTransportPort, then remove the field, the flag, and the CRD entry.

Generated with Claude Code

#6296 moved /metrics to a diagnostics port. That is a breaking change for
any deployment scraping the old location, and it has not shipped yet, so
there is still room to give it a notice window rather than land it cold.

Serve /metrics on both ports for now. The diagnostics listener always
runs; the transport-port copy is controlled by MetricsOnTransportPort and
defaults to on, so no existing scrape configuration breaks. Operators can
move a scraper to the new port, verify it, and set the field to false to
prove nothing else depended on the old one. Closing the window is a
one-line change to DefaultMetricsOnTransportPort.

The field is a pointer with no kubebuilder default, which is what makes
the eventual flip work. RunConfig.TelemetryConfig is serialised and CRD
defaults are materialised at admission, so a plain bool would be written
into every workload created during the window and would survive the
cutover unchanged -- the flip would silently move nobody. Nil means unset
and is resolved at startup instead, with tests covering both the
resolution and the round trip.

Deployments that set the field explicitly are deliberately not moved by
the flip.

Part of #6271

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the size/S Small PR: 100-299 lines changed label Aug 19, 2026
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.82%. Comparing base (b30ca2b) to head (fff7cf2).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
pkg/runner/config_builder.go 60.00% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main    #6371    +/-   ##
========================================
  Coverage   77.82%   77.82%            
========================================
  Files         760      761     +1     
  Lines       72997    73190   +193     
========================================
+ Hits        56808    56960   +152     
- Misses      16184    16225    +41     
  Partials        5        5            

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

amirejaz and others added 3 commits August 19, 2026 15:13
The notice said the transport-port copy would be removed but never said
when, and a deprecation without a deadline is easy to ignore. Users also
had nowhere to look for the plan.

Reference #6384 from the startup warning, the field documentation, and the
migration steps. A tracking issue rather than a version or date: the
release that closes the window is not known yet and may move, and the
issue can be revised where these strings cannot -- the field description
ships to users through the CRD schema and kubectl explain.

Also point DefaultMetricsOnTransportPort at it, since flipping that
constant is not the whole job and the issue carries the cleanup list.

Part of #6271
#6370 added metricsOnTransportPort but left it reachable only by editing
stored configuration, so nobody could actually use the migration window
it exists for.

Add --otel-metrics-on-transport-port and prometheus.metricsOnTransportPort
on MCPTelemetryConfig, so an operator can move a scraper to the
diagnostics port and then turn the old location off to prove nothing else
depended on it.

Both preserve the tri-state. The CLI flag is bound as a plain bool and
read through resolveMetricsOnTransportPort, which uses Flags().Changed so
an absent flag stays unset rather than resolving to false; the CRD field
is an optional pointer with no default marker so nothing is materialised
at admission. Either would otherwise pin the window's value into every
workload created during it, and the cutover would move nobody.

The runtime field is now mapped on both sides, so it moves from the
drift table's runtime-only ignores into the mappings.

Carried by a dedicated builder option rather than a further parameter on
the two telemetry constructors: the field is transitional, and removing
it later should touch one function instead of two long signatures.

Part of #6271

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CLI flag help and the MCPTelemetryConfig field description said the
default would change but not when, and gave nowhere to look for the plan.
Both are user-facing -- the flag through thv run --help and the field
through kubectl explain -- so they need the reference as much as the
startup warning does.

Reference #6384 from both. See its sibling commit on the base branch for
why a tracking issue rather than a version or date.

Part of #6271
@amirejaz
amirejaz force-pushed the metrics-transport-port-optin branch from 061c443 to 35b6e1e Compare August 19, 2026 14:16
@github-actions github-actions Bot added size/S Small PR: 100-299 lines changed and removed size/S Small PR: 100-299 lines changed labels Aug 19, 2026
Base automatically changed from metrics-transport-port-deprecation to main August 20, 2026 12:17
Merging main duplicated the field across both drift tables: main still
lists it in telemetryIgnoredOnRuntimeOnly with a justification saying it
is deliberately absent from the CRD, while this branch already promoted
it into telemetryFieldMappings by adding the CRD field. Remove the stale
ignore entry; the mapping is the current truth.
@github-actions github-actions Bot added size/S Small PR: 100-299 lines changed and removed size/S Small PR: 100-299 lines changed labels Aug 25, 2026

@jhrozek jhrozek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed this with a few specialist passes. The core design is right and I want to say that clearly before the nits: the pointer tri-state with no kubebuilder default is correct, and I verified it actually holds end to end — no defaulting webhook on MCPTelemetryConfig, omitempty keeps nil out of both the applied object under SSA and the persisted RunConfig, so flipping DefaultMetricsOnTransportPort later really will move workloads that already exist. That was the hard part.

Comments below are mostly small. Two bigger things that don't map to a line in this diff:

vMCP won't cut over. pkg/vmcp/server/server.go:610 mounts /metrics on the main mux unconditionally, and ServeMetricsOnTransportPort() is consulted in exactly one place (pkg/runner/diagnostics.go:76). The inline telemetry.Config path already exposed this field on virtualmcpservers.yaml before this PR, so that part isn't yours — but vmcpconfig/converter.go:513 routes TelemetryConfigRef through NormalizeMCPTelemetryConfig, so this PR adds a second path where the field is settable and inert. When #6384 flips the default, MCPServer moves and vMCP silently doesn't. Worth a companion issue.

The migration isn't performable in-cluster yet. docs/observability.md:221 step 1 says point your scraper at the diagnostics port and confirm metrics arrive. Under the operator there's nothing to point at: mcpserver_controller.go:1467 declares only the proxy containerPort and :1561 only the proxy ServicePort, and prometheusPort is intentionally absent from MCPTelemetryConfigSpec. The pod is listening on 9464, it's just undiscoverable — a PodMonitor has no named port to key on. One named containerPort when EnablePrometheusMetricsPath is set would fix it (leave the Service alone, the docs are right that it shouldn't be on internet-facing Services).

Related: step 2 says set it false "and confirm nothing else was still scraping it", but there's no way to confirm that. The startup WARN reports configuration, not usage, and the transport-port /metrics outranks the / catch-all so those scrapes bypass the telemetry middleware entirely. Wrapping the handler at runner.go:388 with a counter is about ten lines and makes that step real.

Neither blocks this PR. Not requesting changes.

Comment thread cmd/thv/app/run_flags.go Outdated
Comment thread cmd/thv-operator/api/v1beta1/mcpserver_types.go Outdated
Comment thread pkg/runner/config_builder.go Outdated
Comment thread cmd/thv-operator/pkg/spectoconfig/telemetry.go Outdated
Comment thread cmd/thv/app/metrics_transport_port_test.go Outdated
Fix the pointer aliasing in NormalizeMCPTelemetryConfig. It assigned
spec.Prometheus.MetricsOnTransportPort straight into config, and
NormalizeTelemetryConfig's "Create a copy to avoid modifying the input"
is only a shallow copy of the struct -- so a future write through the
returned config's pointer would reach back into the CRD spec object. Nil
still means unset, but a non-nil value is now cloned. Added a table case
per state (unset/true/false) plus a dedicated regression test asserting
the result does not alias the input and that mutating it leaves the
input untouched.

Point the CLI flag's default at telemetry.DefaultMetricsOnTransportPort
instead of a hardcoded true, so `thv run --help` stops lying the moment
the cutover flips the constant.

Trim four doc comments back to what the reader actually needs:

- Two exported field docs (telemetry.Config and the CRD's PrometheusConfig)
  drop the "why a pointer with no kubebuilder default" paragraph. Those
  comments become the OpenAPI description under kubectl explain and in
  swagger.json; a cluster admin configuring a scrape target doesn't need
  kubebuilder marker semantics, and the reasoning already lives at its
  canonical home on TestMetricsOnTransportPortNotPersistedWhenUnset.
- WithMetricsOnTransportPort's doc drops the "why a separate option
  instead of a constructor parameter" paragraph, keeping only the actual
  calling contract (apply after telemetry config).
- Two comments citing a review discussion by name (mountPrometheusHandler-
  OnTransportPort's extraction rationale, and a test doc naming jhrozek's
  #6370 comment) are reworded to describe the code on its own terms.

Drop the duplicate ptr[T] helper in metrics_transport_port_test.go; the
package already has boolPtr in run_flags_test.go.

Two findings don't map to a line in this diff and are tracked separately
rather than expanding this PR's scope: vMCP's own switch already exists
on the unmerged #6368, not duplicated here (#6433 tracks landing order);
and the operator doesn't yet expose a named port or a way to verify the
transport-port copy is unused before disabling it (#6434).

Part of #6271

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/S Small PR: 100-299 lines changed labels Aug 26, 2026
@amirejaz
amirejaz merged commit 75ab44b into main Aug 26, 2026
49 checks passed
@amirejaz
amirejaz deleted the metrics-transport-port-optin branch August 26, 2026 10:09
@github-actions github-actions Bot mentioned this pull request Aug 26, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Medium PR: 300-599 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants