Skip to content

Restore transport-port metrics behind a migration switch - #6370

Merged
amirejaz merged 3 commits into
mainfrom
metrics-transport-port-deprecation
Aug 20, 2026
Merged

Restore transport-port metrics behind a migration switch#6370
amirejaz merged 3 commits into
mainfrom
metrics-transport-port-deprecation

Conversation

@amirejaz

Copy link
Copy Markdown
Contributor

Summary

#6296 moved /metrics from the transport port to a dedicated diagnostics port. That
breaks any deployment scraping the old location. It has not shipped yet
v0.44.0 was cut before it merged — so there is still room to give it a notice
window rather than land it cold.

This restores the old location alongside the new one, behind a switch that defaults
to on:

  • The diagnostics listener always runs, so the new location works immediately.
  • metricsOnTransportPort controls the transport-port copy and defaults to on, so
    no existing scrape configuration breaks.
  • A startup warning names the transport port and says the copy is deprecated.

The migration an operator can now follow: point the scraper at the diagnostics port,
confirm metrics arrive, then set metricsOnTransportPort: false to prove nothing else
was still scraping the old one. Closing the window later is a one-line change to
DefaultMetricsOnTransportPort.

A side benefit of dual-serving: the diagnostics listener gets real production exercise
during the window, so bugs in it surface before everyone is moved onto it rather than
after.

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)

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

Special notes for reviewers

The pointer is the load-bearing part, and it is worth a look.

MetricsOnTransportPort is a *bool with no +kubebuilder:default marker. That
is deliberate, because a plain bool would make the eventual cutover a no-op:

  • RunConfig.TelemetryConfig is serialised (pkg/runner/config.go:173,
    WriteJSON at :349), so a resolved value gets written into every workload's
    stored config.
  • A +kubebuilder:default marker is materialised into CRD objects by the API server
    at admission.

Either way, a workload created during the window would carry the window's value
permanently, and changing the default later would move nobody — while looking like it
had. TestMetricsOnTransportPortNotPersistedWhenUnset asserts the unset value is
omitted from the serialised form and still resolves to the current default after a
round trip, so this cannot regress silently.

Deployments that set the field explicitly are deliberately not moved by the flip.
Leaving it unset is what opts you into the new default.

Follow-ups, in order: vMCP needs the same switch (#6368 currently moves it
unconditionally); then the CLI flag and MCPTelemetryConfig field so the opt-in is
reachable without editing stored config; then the flip; then removal of the field.

Drift table: metricsOnTransportPort is classified as runtime-only with a
justification saying it is transitional and should be removed rather than promoted
into the CRD.

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 60.00000% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.73%. Comparing base (7b72d4b) to head (e233ac8).
⚠️ Report is 20 commits behind head on main.

Files with missing lines Patch % Lines
pkg/runner/runner.go 14.28% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6370      +/-   ##
==========================================
+ Coverage   73.04%   77.73%   +4.68%     
==========================================
  Files         744      749       +5     
  Lines       78611    72139    -6472     
==========================================
- Hits        57424    56075    -1349     
+ Misses      17169    16059    -1110     
+ Partials     4018        5    -4013     

☔ 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.

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
@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
amirejaz added a commit that referenced this pull request Aug 19, 2026
#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>
jhrozek
jhrozek previously approved these changes Aug 19, 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.

suggestion: The migration section says /metrics is currently served on both ports, but this table says the transport-port route returns 404. Could we make the row conditional—e.g.
served during the migration window when metricsOnTransportPort is unset/true; 404 when it is false or after cutover? This is fine to address in the next stack PR, but the final stack
should not leave contradictory exposure guidance.

suggestion: The config-resolution tests cover the tri-state, and the stacked PRs cover CLI/vMCP behavior, but the standard runner has no test proving the resolved value reaches
transportConfig.PrometheusHandler . Could a follow-up add unset/true/false coverage around pkg/runner/runner.go:387-388 ? That would catch a regression where the migration switch
resolves correctly but is not mounted by standard workloads.

jhrozek's review on #6296 flagged two things.

The "what lives on the transport port" table said /metrics always 404s
there, contradicting the migration-window section a few paragraphs above
that says it is served on both ports for now. Make the row conditional on
the window rather than stating one state as if it were permanent.

Extract the transport-port mount decision out of Run() into
mountPrometheusHandlerOnTransportPort, and add unset/true/false coverage
for it. The tri-state itself was already tested in pkg/telemetry, and the
stacked PRs test CLI parsing and vMCP's mux, but nothing proved the
resolved value actually reached transportConfig.PrometheusHandler for a
standard workload -- a regression there would leave the switch resolving
correctly while silently never mounting the transport-port copy.

Part of #6271
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/S Small PR: 100-299 lines changed size/M Medium PR: 300-599 lines changed labels Aug 20, 2026
@amirejaz

Copy link
Copy Markdown
Contributor Author

Both addressed — table is now conditional on the migration window, and mountPrometheusHandlerOnTransportPort has unset/true/false coverage. Mind giving it another look? 🙏

@amirejaz
amirejaz merged commit d077c74 into main Aug 20, 2026
93 checks passed
@amirejaz
amirejaz deleted the metrics-transport-port-deprecation branch August 20, 2026 12:17
amirejaz added a commit that referenced this pull request Aug 26, 2026
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>
amirejaz added a commit that referenced this pull request Aug 26, 2026
* Restore transport-port metrics behind a migration switch

#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>

* Point the deprecation notice at a tracking issue

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

* Expose the metrics migration switch to CLI and operator

#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>

* Point the migration switch surfaces at the tracking issue

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

* Drop the now-stale runtime-only drift entry for metricsOnTransportPort

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.

* Address review: real aliasing bug, doc trims, one duplicate helper

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>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@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