feat(collectors): periodically refresh google.projects.filter to pick up new/removed projects without a restart - #540
Merged
kgeckhart merged 5 commits intoSep 21, 2026
Conversation
…up new/removed projects without a restart google.projects.filter was resolved to a concrete project list once at startup, so projects added to or removed from the matching org/folder were never reflected until the exporter restarted. Adds an opt-in google.projects.filter-refresh-interval flag (default 0s, disabled) that re-evaluates the filter on a background ticker and atomically swaps the in-memory project list, and updates the HTTP handler to rebuild its collector registry per scrape so the refreshed list actually reaches /metrics. Fixes prometheus-community#538 Signed-off-by: Pavan Nalam <pavan.nalam3693@gmail.com>
ntmspavan
force-pushed
the
fix/issue-538-projects-filter-refresh
branch
from
September 8, 2026 05:51
5a92ca8 to
a61d4cd
Compare
sthiakos
approved these changes
Sep 17, 2026
Signed-off-by: Pavan Nalam <pavan.nalam3693@gmail.com>
ntmspavan
force-pushed
the
fix/issue-538-projects-filter-refresh
branch
from
September 18, 2026 16:35
d4daade to
2d40e35
Compare
…ck while mutating Get took an RLock (shared among concurrent callers) but still mutated shared state on every call: deleting the map entry on expiry, or writing entry.expiry on a hit. Two goroutines calling Get on the same key concurrently could race on entry.expiry, or both call delete on the same map concurrently, which Go maps do not support and can panic the process with "fatal error: concurrent map writes". This was pre-existing but rarely exercised, since only the ?collect= filtered scrape path went through the cache per request. The prior commit in this branch made the default /metrics path do the same on every scrape, so any overlapping scrapes (HA Prometheus replicas, manual curl during a scheduled scrape, etc.) now hit this far more easily. Every path in Get mutates state, so there is no actual read-only case for RLock to protect; switched to a plain Mutex, matching what Store already used. Added a concurrent-access regression test that reproduces the data race under -race against the old RLock code and passes cleanly with the fix. Signed-off-by: Pavan Nalam <pavan.nalam3693@gmail.com>
ntmspavan
force-pushed
the
fix/issue-538-projects-filter-refresh
branch
from
September 18, 2026 16:57
5f10d3b to
4fa4a40
Compare
kgeckhart
reviewed
Sep 21, 2026
| @@ -222,9 +268,12 @@ func getProjectIDsFromFilter(ctx context.Context, filter string) ([]string, erro | |||
Contributor
There was a problem hiding this comment.
ATM the refresh has no timeout on it and if something stalls on the google side the refresh will be blocked with no real indication of why.
I think the best way to resolve this is create a function to build the cloudresourcemanager service similar to the monitoringservice,
. AFAICT this is safe to create once and hold like we do with the monitoringservice. This should allow us to construct it in the same way with the googleclient timeout + retries. I think universedomain should be skipped.
Contributor
Author
There was a problem hiding this comment.
Thanks @kgeckhart for the feedback! Addressed in latest commit:
- Added
createResourceManagerServicewith consistent timeout/retry logic andCloudPlatformReadOnlyScope. - Initialized service once in
NewRuntimeto reuse across refresh intervals instead of reconstructing per call.
…and retries getProjectIDsFromFilter constructed a bare cloudresourcemanager.Service with cloudresourcemanager.NewService(ctx) on every call: no timeout, no retry handling. It ran once at startup and again on every google.projects.filter-refresh-interval tick, so a stall on Google's side during a refresh would block with no indication why, and the service was needlessly rebuilt from scratch each refresh. Adds createResourceManagerService in service.go, mirroring createMonitoringService: same cfg.HTTPTimeout, MaxRetries, RetryStatuses, BackoffJitter, and MaxBackoff wiring via rehttp, using CloudPlatformReadOnlyScope (least-privilege) instead of the broader default scope the bare constructor fell back to, and skipping UniverseDomain. NewRuntime now constructs it once, only when ProjectsFilter is set, and closes over it for both the initial resolution and every later refresh via the existing discoverProjectIDs seam. Signed-off-by: Pavan Nalam <pavan.nalam3693@gmail.com>
…* timeout/retry settings The stackdriver.http-timeout, stackdriver.max-retries, and stackdriver.retry-statuses descriptions read as Monitoring-API-only, but createResourceManagerService now applies the same settings to resolving and refreshing google.projects.filter. Signed-off-by: Pavan Nalam <pavan.nalam3693@gmail.com>
ntmspavan
force-pushed
the
fix/issue-538-projects-filter-refresh
branch
from
September 21, 2026 14:54
d5e752c to
44eedd8
Compare
kgeckhart
approved these changes
Sep 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem & Impact
google.projects.filteris resolved to a concrete project list once at startup. In long-lived Kubernetes deployments, projects added to or removed from the matching org/folder are never reflected until the pod restarts — new projects silently go unmonitored and deleted ones are queried needlessly.Fixes #538
Solution
Adds an opt-in
google.projects.filter-refresh-intervalflag (default0, disabled — fully backward compatible). When set andgoogle.projects.filteris configured, a background goroutine re-resolves the filter on that interval and atomically swaps the in-memory project list (lock-freeatomic.Pointer[[]string], safe under concurrent scrapes). A transient API error or an unexpectedly empty result never overwrites a known-good list. The HTTP handler now rebuilds its collector registry per scrape (as the?collect=-filtered path already did) so the refreshed list actually reaches/metrics.Verification
go build ./...go vet ./...go test ./... -race(new tests cover refresh success/error/empty-result handling, concurrent access, and a regression test for WithCache pointer sharing)Live Testing
Verified end-to-end against a real GCP organization (not just unit tests), using a binary built from this branch (
fix/issue-538-projects-filter-refresh, commit5a92ca8).Tool version:
stackdriver_exporterbuilt from commit5a92ca81002d04a83fcc820e0fedf8feb24fc37f,go1.25.8,darwin/arm64.Setup: created two throwaway GCP projects under a real organization and ran the exporter against the org with:
Before: only project A exists
Add project B — no exporter restart
Created a second project (
sdexp-refresh-b-1788844331) under the same org while the exporter kept running (same PID throughout). GCP's Resource Manager list API is eventually consistent — it took ~75s for the new project to become visible toprojects.list, which the background refresh then picked up on its next tick:Remove project B — no exporter restart
Deleted project B (still the same running process). The next refresh dropped it automatically:
This is the exact bug from #538 reproduced and fixed: previously, this whole cycle would have required restarting the exporter for either the addition or removal of project B to ever show up in
/metrics. Here it self-heals within one refresh interval, with the exporter process never restarted (single PID for the full test).