Skip to content

fix(prometheus): keep looking for a Prometheus the agent could not find at boot - #601

Open
mayankpande88 wants to merge 4 commits into
mainfrom
fix/prometheus-late-discovery
Open

fix(prometheus): keep looking for a Prometheus the agent could not find at boot#601
mayankpande88 wants to merge 4 commits into
mainfrom
fix/prometheus-late-discovery

Conversation

@mayankpande88

Copy link
Copy Markdown
Contributor

Summary

Prometheus service discovery ran once, at startup, and that one result decided for the life of the process whether prometheus_* actions, the enrichers, right-sizing and the service map were registered at all.

So an agent that started before its Prometheus stayed blind to it until someone restarted the pod. That happens more easily than it sounds: a fresh cluster where the agent and kube-prometheus-stack are installed together (the Service does not exist yet when we look), or any cluster where monitoring is added afterwards. The only trace was a single WARN in the boot log — from the outside it just looked like the product had no metrics: empty charts, and no right-sizing recommendations on workloads that plainly needed them.

Discovery is no longer a one-shot decision:

  • The client is always constructed, with or without a URL, so its actions are always registered. Calls before a URL is known fail with prometheus: base URL not configured rather than unknown action.
  • Its base URL is settable at runtime (RWMutex-guarded, read fresh per request), so a URL found later needs no re-registration and no restart.
  • When no URL is configured at boot, a watcher re-runs discovery every 5 minutes and points the client at the first Prometheus that appears.

service_map, alert_rules and continuous_rightsizing lose their promClient != nil gates accordingly — each already reports the missing URL itself.

The install script is unaffected: it still passes --set globalConfig.prometheus_url=..., and an explicitly configured URL short-circuits all of this.

Type of change

  • Bug fix (non-breaking)

Chart version

  • No version bump needed (runner code only)

Test plan

make validate passes in runner/; full suite green under -race; golangci-lint run clean on the changed files (the one repo-wide gosec G704 in pkg/observability/signoz/client.go is pre-existing and untouched).

Tests added:

  • TestWatchUntilFound_PicksUpAServiceAddedLater — nothing to find, then a Prometheus Service appears; the watcher reports it.
  • TestWatchUntilFound_DoesNotInheritTheNegativeCache — guards the subtle part. Verified by regression: rewriting the watcher to reuse one Discoverer makes it fail, because FindFirst caches misses for an hour and would freeze the retry after the first attempt for the rest of the process's life.
  • TestSetURL_MakesAnUnconfiguredClientWork — a client built with no URL rejects queries, then serves them after SetURL, with the trailing slash trimmed.
  • TestSetURL_IsSafeWhileQueriesRun — concurrent SetURL and Query under -race, since the watcher writes the URL from its own goroutine while handlers read it.

How to verify on a cluster

  1. Install the agent on a cluster with no Prometheus and no globalConfig.prometheus_url. The runner logs prometheus not configured: ... retry_interval=5m0s and the account's Prometheus health badge reads disconnected.
  2. Ask it for metrics — the action is registered and answers prometheus: base URL not configured rather than failing as an unknown action.
  3. helm install kube-prometheus-stack into that cluster and wait up to 5 minutes.
  4. The runner logs prometheus discovered after startup ... url=http://...:9090 with no pod restart, the health badge flips to connected, metrics queries answer, and right-sizing recommendations start appearing on the next scan.

Related issues

None open for this. Found while verifying a customer evaluation report of "metrics unavailable" against the code.

@mayankpande88
mayankpande88 requested a review from a team as a code owner September 4, 2026 08:03

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces dynamic Prometheus discovery after startup, allowing the agent to register Prometheus actions unconditionally and update the client's URL in the background once a Prometheus service is discovered. Feedback on these changes points out that other components initialized at startup, such as the Telemetry Poster and Grafana Proxy, still rely on the static configuration URL and will not receive the dynamically discovered URL, which could lead to incomplete telemetry and failing proxied queries.

Comment thread runner/cmd/agent/main.go
@mayankpande88

Copy link
Copy Markdown
Contributor Author

Good catch on both — that was a real gap I introduced, since the boot-time path used to write the discovered URL back into cfg and the watcher deliberately does not (other goroutines read cfg concurrently).

Fixed in 57a779f:

  1. Telemetry — now reports promClient.URL(). It is evaluated inside the per-tick Datasources closure, so it tracks discovery without touching cfg.
  2. Grafana proxy — rather than refactor New's signature, Proxy gains an optional PrometheusURLFn func() string consulted per request; main wires it to promClient.URL. The static PrometheusURL field still serves every other caller and every existing test. Covered by TestProxy_HandlePrometheus_UsesTheURLLookupWhenSet, which 503s before discovery and forwards after, and pins the trailing-slash trim so the base and path don't double up.

I checked the other two cfg.PrometheusURL reads: line 217 is boot-time discovery itself and line 270 is the client constructor, both before the watcher can run, so neither needs the same treatment.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📦 Image Tags Updated

I've automatically updated the image tags in `charts/nudgebee-agent/values.yaml` to the latest versions from GHCR for the `main` branch.

The image tags are now synchronized with the latest builds and ready for release.

…nd at boot

Service discovery ran exactly once, during startup, and the result decided
for the life of the process whether prometheus_* actions, the enrichers,
rightsizing and the service map were registered at all. An agent that came
up before its Prometheus — a fresh cluster where both are installed
together, or a cluster where monitoring is added later — therefore stayed
blind to it until someone restarted the pod. The only trace was one warning
in the boot log, so from the outside it looked like the product simply had
no metrics: "no data" on charts, and no right-sizing recommendations on
workloads that obviously needed them.

Discovery is no longer a decision made once:

  - The Prometheus client is always constructed, with or without a URL, so
    its actions are always registered. Calls made before a URL is known fail
    with "prometheus: base URL not configured" instead of "unknown action".
  - Its base URL is settable at runtime (guarded by an RWMutex; every request
    reads it fresh), so a URL found later needs no re-registration.
  - When no URL is configured at boot, a watcher re-runs discovery every 5
    minutes and points the client at the first Prometheus that appears. It
    builds a fresh Discoverer per attempt: FindFirst caches misses for an
    hour, which would otherwise freeze the retry after the first one.

Service-map, alert-rules and continuous_rightsizing lose their promClient
!= nil gates accordingly; each already reports the missing URL itself. The
health badge is unaffected — prometheusConnected still probes the live URL,
so it stays false until one is found.
…d telemetry

Both read cfg.PrometheusURL, which the watcher does not touch — it cannot,
without racing the goroutines that read cfg. So on a late discovery the
/prometheus-v2/* proxy route kept 503-ing and telemetry kept reporting an
empty Prometheus URL alongside a connected badge.

Telemetry now reports promClient.URL(), evaluated per tick. The proxy gains
an optional PrometheusURLFn consulted per request, which main wires to
promClient.URL; the static field stays for every other caller.
@mayankpande88
mayankpande88 force-pushed the fix/prometheus-late-discovery branch from 57a779f to 7b9dea8 Compare September 5, 2026 06:54
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