Skip to content

atecontroller: the :8080 metrics listener follows OTEL_METRICS_EXPORTER - #1976

Merged
Jeff Luo (JeffLuoo) merged 9 commits into
agent-substrate:mainfrom
ericcurtin:atecontroller-no-metrics-listener
Oct 13, 2026
Merged

Jeff Luo (JeffLuoo) merged 9 commits into
agent-substrate:mainfrom
ericcurtin:atecontroller-no-metrics-listener

Conversation

@ericcurtin

@ericcurtin Eric Curtin (ericcurtin) commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

The :8080 listener follows OTEL_METRICS_EXPORTER, per the policy in #2067. Unset keeps push and :8080, otlp closes :8080, prometheus serves only, none does neither.

Fixes #933

  • Tests pass
  • Appropriate changes to documentation are included in the PR

Its registry leaves over OTLP, so nothing scrapes :8080. Drop the
Service and port too.
@ericcurtin

Copy link
Copy Markdown
Contributor Author

Jeff Luo (@JeffLuoo) Lior Lieberman (@LiorLieberman) PTAL when you get a chance. Thank you!

@JeffLuoo

Copy link
Copy Markdown
Collaborator

Thank you for this PR, and for the cleanup in managerOptions.

I suggest that we hold this PR until #2067 decides the metrics policy.

@ericcurtin

Copy link
Copy Markdown
Contributor Author

Sounds good, holding until #2067 is decided. Will rebase after.

@ericcurtin

Copy link
Copy Markdown
Contributor Author

Merged main and resolved the conflict. Still holding for #2067.

@JeffLuoo

Jeff Luo (JeffLuoo) commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Thank you for holding this. #2067 has decided the policy, and task 3 there replaces this PR. :8080 must follow OTEL_METRICS_EXPORTER, not always turn off. There are multiple PRs at the moment trying to add a function to parse OTel standard environment variables. I will suggest still hold until the env var parsing part is done, and then rework this PR.

@ericcurtin

Copy link
Copy Markdown
Contributor Author

Jeff Luo (@JeffLuoo) Understood, thanks. I will hold until the OTel env var parsing lands and then rework this.

@JeffLuoo

Jeff Luo (JeffLuoo) commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Eric Curtin (@ericcurtin)

Thank you for waiting. Here is the rework, based on the policy in #2067. Please rebase after #2190 merges. That PR moves OTEL_METRICS_EXPORTER to the shared parser and makes prometheus a known name.

@JeffLuoo

Copy link
Copy Markdown
Collaborator

The change I mentioned is merged.

@ericcurtin Eric Curtin (ericcurtin) changed the title atecontroller: disable the unused metrics listener atecontroller: the :8080 metrics listener follows OTEL_METRICS_EXPORTER Oct 8, 2026
@ericcurtin

Copy link
Copy Markdown
Contributor Author

Jeff Luo (@JeffLuoo) Reworked on top of #2190, PTAL.

@JeffLuoo Jeff Luo (JeffLuoo) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The implementation and tests look good. Two comments on docs/observability.md inline.

Comment thread docs/observability.md Outdated
### Scraping instead of pushing

ateapi, atelet, atenet-router and the credential provider also serve every instrument on their Prometheus `/metrics` endpoint. A cluster that scrapes those endpoints sets `OTEL_METRICS_EXPORTER=none` on the components, so each series reaches the backend once. With `none` the components install no OTLP metric reader and keep the Prometheus one; traces and logs are unaffected. atecontroller then registers its instruments (`ate.workerpool.*`) on controller-runtime's registry, so the manager's `:8080` serves them next to the controller-runtime families. ateom serves no endpoint of its own, so leave the variable unset on the worker pods, or it exports no metrics at all. The variable takes a comma-separated list, with the same rules as `OTEL_LOGS_EXPORTER`. `otlp` (the default) keeps the push, and `prometheus` or `none` stops it. `/metrics` stays on for each value. ateom serves no `/metrics`, so there `prometheus` is skipped as unknown with a warning, and the push stays on unless the variable is `none`.
ateapi, atelet, atenet-router and the credential provider also serve every instrument on their Prometheus `/metrics` endpoint. A cluster that scrapes those endpoints sets `OTEL_METRICS_EXPORTER=prometheus` on the components, so each series reaches the backend once. With `prometheus` the components install no OTLP metric reader and keep the Prometheus one; traces and logs are unaffected. `none` stops the push too, and leaves their `/metrics` on. ateom serves no endpoint of its own, so leave the variable unset on the worker pods, or it exports no metrics at all. The variable takes a comma-separated list, with the same rules as `OTEL_LOGS_EXPORTER`. `otlp` (the default) keeps the push, and `prometheus` or `none` stops it. ateom serves no `/metrics`, so there `prometheus` is skipped as unknown with a warning, and the push stays on unless the variable is `none`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Two stale details in this paragraph:

  1. "ateom serves no endpoint of its own, so leave the variable unset on the worker pods, or it exports no metrics at all" was written when none was the scrape-only setting. Two sentences later, this paragraph notes that ateom skips prometheus as unknown and keeps the push on unless the variable is none (and atecontroller does not propagate OTEL_METRICS_EXPORTER to worker pods). Please remove that sentence or scope it to none.
  2. "otlp (the default) keeps the push" — with metricsExporters setting def = Exporters{ExporterOTLP: true, exporterPrometheus: true} for components that serve /metrics, unset defaults to otlp,prometheus (otlp only on ateom), which matters because the next paragraph contrasts Unset with otlp.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a9ea0d7, thanks.

Comment thread docs/observability.md Outdated
ateapi, atelet, atenet-router and the credential provider also serve every instrument on their Prometheus `/metrics` endpoint. A cluster that scrapes those endpoints sets `OTEL_METRICS_EXPORTER=none` on the components, so each series reaches the backend once. With `none` the components install no OTLP metric reader and keep the Prometheus one; traces and logs are unaffected. atecontroller then registers its instruments (`ate.workerpool.*`) on controller-runtime's registry, so the manager's `:8080` serves them next to the controller-runtime families. ateom serves no endpoint of its own, so leave the variable unset on the worker pods, or it exports no metrics at all. The variable takes a comma-separated list, with the same rules as `OTEL_LOGS_EXPORTER`. `otlp` (the default) keeps the push, and `prometheus` or `none` stops it. `/metrics` stays on for each value. ateom serves no `/metrics`, so there `prometheus` is skipped as unknown with a warning, and the push stays on unless the variable is `none`.
ateapi, atelet, atenet-router and the credential provider also serve every instrument on their Prometheus `/metrics` endpoint. A cluster that scrapes those endpoints sets `OTEL_METRICS_EXPORTER=prometheus` on the components, so each series reaches the backend once. With `prometheus` the components install no OTLP metric reader and keep the Prometheus one; traces and logs are unaffected. `none` stops the push too, and leaves their `/metrics` on. ateom serves no endpoint of its own, so leave the variable unset on the worker pods, or it exports no metrics at all. The variable takes a comma-separated list, with the same rules as `OTEL_LOGS_EXPORTER`. `otlp` (the default) keeps the push, and `prometheus` or `none` stops it. ateom serves no `/metrics`, so there `prometheus` is skipped as unknown with a warning, and the push stays on unless the variable is `none`.

atecontroller's `:8080` listener follows the variable. Unset, or a list with both, it pushes and serves; `otlp` pushes and closes the listener; `prometheus` serves only, and registers its instruments (`ate.workerpool.*`) on controller-runtime's registry, so `:8080` serves them next to the controller-runtime families; `none` does neither.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

When the variable is unset or otlp,prometheus, :8080 is open but does not serve the ate.workerpool.* instruments: because OTLP push is enabled, InitMetricsBridged keeps the OTel instruments off ctrlmetrics.Registry to avoid pushing a second copy through the bridge, and only registers them on :8080 under prometheus alone. Saying "it pushes and serves" makes a reader expect the full set on :8080 in the default mode. Please state which metrics :8080 serves in each mode:

Suggested change
atecontroller's `:8080` listener follows the variable. Unset, or a list with both, it pushes and serves; `otlp` pushes and closes the listener; `prometheus` serves only, and registers its instruments (`ate.workerpool.*`) on controller-runtime's registry, so `:8080` serves them next to the controller-runtime families; `none` does neither.
atecontroller's `:8080` listener follows the variable. When the variable is unset or `otlp,prometheus`, atecontroller pushes every metric over OTLP, and `:8080` serves only the controller-runtime families. With `otlp`, atecontroller pushes every metric and closes `:8080`. With `prometheus`, atecontroller does not push, and `:8080` serves the controller-runtime families and the `ate.workerpool.*` instruments. With `none`, atecontroller does not push and closes `:8080`.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a9ea0d7, thanks.

}

// servePull follows the policy table: unset and the list keep both paths.
func TestInitMetricsBridgedServePull(t *testing.T) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please add a name field. The empty value row currently shows up as subtest #00.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a46d59d, thanks.

exporters := metricsExporters(ctx, true)
servePull = exporters.Has(exporterPrometheus)
switch {
case exporters.Has(ExporterOTLP):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In this branch :8080 misses ate.workerpool.*, so unset and otlp,prometheus are not following the policy. This can be a follow-up, but please open an issue to track this in that case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Tracked in #2402, thanks. A fix needs a separate registry to avoid pushing them twice.

Comment thread cmd/atecontroller/main.go
// own Prometheus registry. OTEL_METRICS_EXPORTER picks where they go: the
// OTLP push, which pads the bridged queue histograms so the Telemetry API
// accepts idle ones, and the manager's scrape listener.
mp, servePull, err := serverboot.InitMetricsBridged(ctx, serviceName, ctrlmetrics.Registry, padEmptyExponentialHistograms)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's log the resolved exporters and whether :8080 is on.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a46d59d, thanks.

Comment thread cmd/atecontroller/main.go Outdated
func managerOptions(egressMITMCAPool types.NamespacedName, servePull bool) ctrl.Options {
metricsAddr := "0" // "0" disables the server.
if servePull {
metricsAddr = metricsserver.DefaultBindAddress

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: we could use 8080 as a const that we can reference in tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a46d59d, thanks.

Comment thread cmd/atecontroller/main_test.go Outdated
Comment on lines +91 to +92
for _, servePull := range []bool{true, false} {
opts := managerOptions(types.NamespacedName{Namespace: "ate-system", Name: "pool"}, servePull)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's rewrite as a tests := []struct{...} table with t.Run, following the repo's convention.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a46d59d, thanks.

Comment thread docs/observability.md Outdated
### Scraping instead of pushing

ateapi, atelet, atenet-router and the credential provider also serve every instrument on their Prometheus `/metrics` endpoint. A cluster that scrapes those endpoints sets `OTEL_METRICS_EXPORTER=none` on the components, so each series reaches the backend once. With `none` the components install no OTLP metric reader and keep the Prometheus one; traces and logs are unaffected. atecontroller then registers its instruments (`ate.workerpool.*`) on controller-runtime's registry, so the manager's `:8080` serves them next to the controller-runtime families. ateom serves no endpoint of its own, so leave the variable unset on the worker pods, or it exports no metrics at all. The variable takes a comma-separated list, with the same rules as `OTEL_LOGS_EXPORTER`. `otlp` (the default) keeps the push, and `prometheus` or `none` stops it. `/metrics` stays on for each value. ateom serves no `/metrics`, so there `prometheus` is skipped as unknown with a warning, and the push stays on unless the variable is `none`.
ateapi, atelet, atenet-router and the credential provider also serve every instrument on their Prometheus `/metrics` endpoint. A cluster that scrapes those endpoints sets `OTEL_METRICS_EXPORTER=prometheus` on the components, so each series reaches the backend once. With `prometheus` the components install no OTLP metric reader and keep the Prometheus one; traces and logs are unaffected. `none` stops the push too, and leaves their `/metrics` on. The variable takes a comma-separated list, with the same rules as `OTEL_LOGS_EXPORTER`. Unset is `otlp,prometheus`, or `otlp` alone on ateom. `otlp` keeps the push, and `prometheus` or `none` stops it. ateom serves no `/metrics`, so there `prometheus` is skipped as unknown with a warning, and the push stays on unless the variable is `none`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This conflicts with what we discussed over at #2067 where none means no /metrics. Please it as temporary until we move the probes off :9090, and link #2067, so readers don't take it as the policy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a46d59d, thanks.

Name the table rows, make :8080 a const, log the resolved exporters, and
mark the /metrics note temporary (agent-substrate#2067).
@ericcurtin

Copy link
Copy Markdown
Contributor Author

Jeff Luo (@JeffLuoo) Krisztian F (@krisztianfekete) Addressed the review, PTAL when you get a chance. Thank you!

@JeffLuoo Jeff Luo (JeffLuoo) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Just a few more nits

Comment thread docs/observability.md Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Here it still says "which the manager serves on an unscraped :8080". Now that :8080 is disabled under otlp/none and scraped under prometheus, could we update the text here to match line 338?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5529baa, thanks.

Comment thread cmd/atecontroller/main.go Outdated
serverboot.Fatal(ctx, "Failed to initialize metrics", err)
}
defer serverboot.ShutdownProvider("MeterProvider", mp.Shutdown)
slog.InfoContext(ctx, "Metrics listener", slog.Bool("enabled", servePull), slog.String("addr", metricsPullAddr))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

When servePull is false, managerOptions binds to metricsOffAddr ("0"), so logging addr=":8080" alongside enabled=false is misleading. Consider logging the resolved opts.Metrics.BindAddress (or omitting addr when disabled).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5529baa, logs the resolved address.

Comment thread internal/serverboot/serverboot.go Outdated
}
if metricsPushEnabled(ctx, true) {
exporters := metricsExporters(ctx, true)
slog.InfoContext(ctx, "Metrics exporters resolved", slog.String("exporters", exporters.String()))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit - if we move this log into metricsExporters(ctx) (or newMeterProvider), InitMetrics and InitMetricsPushOnlyVia will also log the resolved exporters at startup, not just atecontroller.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5529baa, moved into metricsExporters.

@ericcurtin

Copy link
Copy Markdown
Contributor Author

Jeff Luo (@JeffLuoo) Krisztian F (@krisztianfekete) Addressed, PTAL. The e2e failure was httpbin.org returning EOF, unrelated; the new push reruns it.

@@ -445,10 +445,13 @@ func bridgedRegistry(t *testing.T) *prometheus.Registry {
func TestInitMetricsBridgedPushesEachMetricOnce(t *testing.T) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's make this table-driven over "", otlp and otlp,prometheus. If the code registered the OTel reader on reg in the default mode, every instrument would be pushed twice, and no test would fail.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in bec44ef, now covers unset, otlp and otlp,prometheus.

@ericcurtin

Copy link
Copy Markdown
Contributor Author

Merged main. The e2e failure was a restore wakeup probe timeout in TestMultipleDurableDirLifecycle, unrelated to the listener; the push reruns it.

@JeffLuoo
Jeff Luo (JeffLuoo) added this pull request to the merge queue Oct 13, 2026
Merged via the queue into agent-substrate:main with commit 626ce68 Oct 13, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/observability kind/cleanup Small fixes that are not bugs, for example a typo in a code comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

atecontroller metrics are never scraped

4 participants