Skip to content

google: sync export with bernot-dev/release-3.13.0-gmp - #330

Draft
bwplotka wants to merge 1 commit into
release-2.53.5-gmpfrom
export-check
Draft

google: sync export with bernot-dev/release-3.13.0-gmp#330
bwplotka wants to merge 1 commit into
release-2.53.5-gmpfrom
export-check

Conversation

@bwplotka

@bwplotka bwplotka commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

This is only for preview on what changed vs release-2.53.5-gmp so far against 4ac9c11 commit

Executive Summary & Stats

Syncs the google/ directory with bernot-dev/release-3.13.0-gmp. In total, 15 files changed under google/, with 1,056 insertions and 982 deletions:

File Path Status Changes
google/{ => export}/config/config.go Renamed 0 (100% similarity move)
google/export/export.go Modified +23 / -28
google/export/transform.go Modified +60 / -91
google/export/export_test.go Modified +33 / -33
google/export/series_cache_test.go Modified +7 / -7
google/export/export_bench_test.go Deleted -195
google/export/transform_test.go Modified +8 / -618
google/export/gcm/export_gcm_test.go Added +117
google/export/gcm/promtest/local_export.go Added +227
google/export/gcm/promtest/prometheus.go Added +188
google/export/gcm/promtest/promtest.go Added +332
google/export/gcm/promtest/skip.go Added +43
google/internal/promqle2etest/backend_export_gcm.go Modified +4 / -4
google/internal/promqle2etest/Makefile Modified +1 / -1
google/secrets/watch.go Modified +12 / -6

Architectural Breakdown of Changes

1. Prometheus v3 Upstream API Migration

Between Prometheus 2.53.x and 3.13.x, upstream API signatures and struct definitions changed:

  • MetricMetadata Field Rename: The field Metric string in MetricMetadata was renamed to MetricFamily string to align with OpenMetrics / Prometheus v3 terminology.
  • PromQL Parser Instantiation: parser.ParseMetricSelector(s) in Matchers.Set was migrated to parser.NewParser(parser.Options{}).ParseMetricSelector(s).
  • Text/Proto Parser Options: textparse.New(...) and textparse.Parser method calls in backend_export_gcm.go were updated to accept textparse.ParserOptions{} and tp.Labels(&lset) instead of tp.Metric(&lset).

2. Package Organization (config move)

  • google/config/config.go was moved into google/export/config/config.go to cleanly namespace the Google Cloud Monitoring (GCM) exporter configuration under the export package.

3. Exporter Disable No-Op Guard (export.go)

  • Added a fast-path guard in Exporter.ApplyConfig(cfg *config.Config) (if e.opts.Disable { return nil }), preventing unnecessary lock acquisition and runtime re-initialization when GCM export is disabled via flags.

4. Histogram Distribution Processing Refactor (transform.go)

  • Single Distribution Construction: Refactored buildDistributions into buildDistribution. Instead of accumulating all completed histogram series (histResultsBuf) across an entire batch inside transform.go, buildDistribution now returns upon completing the first full histogram distribution along with its resetTimestamp.
  • Memory & Cache Footprint Reduction: Removed redundant hash, proto, and lset fields from the internal distribution cache object, reducing heap allocations during high-throughput histogram scraping.
  • Streamlined next() Loop: The sampleBuilder.next() method now directly checks if buildDistribution produced a distribution value and wraps it into a monitoring_pb.TypedValue_DistributionValue on demand.

5. Kubernetes Secret Watcher Safety (watch.go)

  • Name Filtering & Validation: Improved secretWatcher.update and secret() methods in google/secrets/watch.go to explicitly verify secret.Name == config.Name. This ensures updates or errors from unrelated secrets in the same namespace do not corrupt or overwrite the cached secret value.

6. Test Suite & E2E Reorganization (google/export/gcm/)

  • Legacy benchmarks and unit tests (export_bench_test.go and 618 lines of transform_test.go) were removed and replaced with a dedicated, robust end-to-end testing framework under google/export/gcm/ (export_gcm_test.go and promtest/*).

TAG=agy
CONV=2002b781-f79e-47f1-823f-25677b6b5cb8

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request refactors the Google Cloud monitoring exporter configurations, updates metric metadata structures to use MetricFamily, and simplifies histogram distribution building in transform.go. It also introduces a comprehensive testing framework under google/export/gcm/promtest. Feedback on these changes includes addressing potential panics in test helpers by avoiding global signal handlers and asynchronous t.Logf calls, resolving variable shadowing of the testing object, properly clearing and recycling completed distributions from the cache, correcting a misleading error message, and optimizing test retry polling intervals to reduce test suite latency.

func (l *localExportWithGCM) start(t testing.TB, _ e2e.Environment) (v1.API, map[string]string) {
t.Helper()

ctx, cancel := context.WithCancel(signals.SetupSignalHandler())

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.

high

Using signals.SetupSignalHandler() in a test helper is highly discouraged. SetupSignalHandler registers global signal handlers and panics if called more than once in the same test binary (which happens if multiple tests use this helper or if tests are run with -count). It should be replaced with context.Background().

Suggested change
ctx, cancel := context.WithCancel(signals.SetupSignalHandler())
ctx, cancel := context.WithCancel(context.Background())

Comment on lines +120 to +124
go func() {
if err := l.e.Run(); err != nil {
t.Logf("running exporter: %s", err)
}
}()

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.

medium

Calling t.Logf inside an asynchronous goroutine that may run during or after the test cleanup phase can cause a panic (panic: Logf called after test finished). It is safer to log to os.Stderr or use a standard logger.

Suggested change
go func() {
if err := l.e.Run(); err != nil {
t.Logf("running exporter: %s", err)
}
}()
go func() {
if err := l.e.Run(); err != nil {
fmt.Fprintf(os.Stderr, "running exporter: %s\n", err)
}
}()

Comment on lines +205 to +218
t := timestamp.FromTime(time.Now())
_, parsedTimestamp, val := tp.Series()
if parsedTimestamp != nil {
t = *parsedTimestamp
}
metadata[currMeta.MetricFamily] = currMeta

lset := labels.New()
tp.Labels(&lset)
l.labelsByRef[storage.SeriesRef(ref)] = lset

batch = append(batch, record.RefSample{
Ref: chunks.HeadSeriesRef(ref), V: val, T: t,
})

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.

medium

The variable t (representing the timestamp) shadows the testing.TB parameter t of the enclosing function injectScrapes. This makes it impossible to use the testing object t for assertions or logging within this block. Rename the timestamp variable to ts to avoid shadowing.

Suggested change
t := timestamp.FromTime(time.Now())
_, parsedTimestamp, val := tp.Series()
if parsedTimestamp != nil {
t = *parsedTimestamp
}
metadata[currMeta.MetricFamily] = currMeta
lset := labels.New()
tp.Labels(&lset)
l.labelsByRef[storage.SeriesRef(ref)] = lset
batch = append(batch, record.RefSample{
Ref: chunks.HeadSeriesRef(ref), V: val, T: t,
})
ts := timestamp.FromTime(time.Now())
_, parsedTimestamp, val := tp.Series()
if parsedTimestamp != nil {
ts = *parsedTimestamp
}
metadata[currMeta.MetricFamily] = currMeta
lset := labels.New()
tp.Labels(&lset)
l.labelsByRef[storage.SeriesRef(ref)] = lset
batch = append(batch, record.RefSample{
Ref: chunks.HeadSeriesRef(ref), V: val, T: ts,
})

Comment thread google/export/transform.go Outdated
Comment on lines +479 to +486
if !dist.complete() {
continue
}
dp, err := dist.build(dist.lset)
dp, err := dist.build(e.lset)
if err != nil {
return nil, samples[consumed:], err
}
if dp != nil && dist.proto != nil {
//nolint:govet
ts := *dist.proto
ts.Points = []*monitoring_pb.Point{{
Interval: &monitoring_pb.TimeInterval{
StartTime: getTimestamp(dist.resetTimestamp),
EndTime: getTimestamp(dist.timestamp),
},
Value: &monitoring_pb.TypedValue{
Value: &monitoring_pb.TypedValue_DistributionValue{DistributionValue: dp},
},
}}
b.histResultsBuf = append(b.histResultsBuf, hashedSeries{hash: dist.hash, proto: &ts})
return nil, 0, samples[consumed:], err
}
return dp, dist.resetTimestamp, samples[consumed:], nil

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.

medium

When a distribution is completed and returned, it is not removed from b.dists. This means stale state remains in the map, which can cause subsequent scrapes of the same histogram in the same batch to fail or be skipped due to mismatching timestamps. Additionally, immediately recycling the completed distribution back to the pool reduces memory pressure. We should delete the completed distribution from b.dists and call putDistribution(dist) before returning.

Suggested change
if !dist.complete() {
continue
}
dp, err := dist.build(dist.lset)
dp, err := dist.build(e.lset)
if err != nil {
return nil, samples[consumed:], err
}
if dp != nil && dist.proto != nil {
//nolint:govet
ts := *dist.proto
ts.Points = []*monitoring_pb.Point{{
Interval: &monitoring_pb.TimeInterval{
StartTime: getTimestamp(dist.resetTimestamp),
EndTime: getTimestamp(dist.timestamp),
},
Value: &monitoring_pb.TypedValue{
Value: &monitoring_pb.TypedValue_DistributionValue{DistributionValue: dp},
},
}}
b.histResultsBuf = append(b.histResultsBuf, hashedSeries{hash: dist.hash, proto: &ts})
return nil, 0, samples[consumed:], err
}
return dp, dist.resetTimestamp, samples[consumed:], nil
if !dist.complete() {
continue
}
dp, err := dist.build(e.lset)
if err != nil {
delete(b.dists, e.protos.cumulative.hash)
putDistribution(dist)
return nil, 0, samples[consumed:], err
}
resetTimestamp := dist.resetTimestamp
delete(b.dists, e.protos.cumulative.hash)
putDistribution(dist)
return dp, resetTimestamp, samples[consumed:], nil

Comment thread google/export/gcm/promtest/promtest.go Outdated
Comment on lines +149 to +151
if it.currTime.Add(after).After(time.Now()) {
it.t.Fatal(errors.New("sum of all scrape 'after' parameters can be beyond 10 hours"))
}

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.

medium

The error message "sum of all scrape 'after' parameters can be beyond 10 hours" is misleading. Since it.currTime is initialized to time.Now().Add(-1 * time.Hour), the maximum simulated time that can be added is 1 hour before it goes into the future. The error message should accurately reflect that the simulated time cannot go into the future.

Suggested change
if it.currTime.Add(after).After(time.Now()) {
it.t.Fatal(errors.New("sum of all scrape 'after' parameters can be beyond 10 hours"))
}
if it.currTime.Add(after).After(time.Now()) {
it.t.Fatal(errors.New("sum of all scrape 'after' parameters cannot go into the future"))
}

ctx, cancel := context.WithTimeout(t.Context(), timeout)
t.Cleanup(cancel)

if err := runutil.Retry(10*time.Second, ctx.Done(), func() error {

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.

medium

Polling every 10 seconds in a test retry loop is extremely slow and can significantly increase test execution times. Since the Prometheus scrape interval in the test configuration is 5 seconds, polling every 1 second is much more efficient and allows the test to complete almost immediately after the last scrape is processed.

Suggested change
if err := runutil.Retry(10*time.Second, ctx.Done(), func() error {
if err := runutil.Retry(1*time.Second, ctx.Done(), func() error {

Comment thread google/export/gcm/promtest/promtest.go Outdated

var lastDiff string
var sameDiffTimes int
if err := runutil.Retry(10*time.Second, ctx.Done(), func() error {

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.

medium

Polling GCM every 10 seconds in the retry loop is very slow. Polling more frequently (e.g., every 2 seconds) would allow the test to detect completion much faster and reduce overall test suite latency.

Suggested change
if err := runutil.Retry(10*time.Second, ctx.Done(), func() error {
if err := runutil.Retry(2*time.Second, ctx.Done(), func() error {

@@ -0,0 +1,47 @@
package config

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Do we have to change file location?

// See the License for the specific language governing permissions and
// limitations under the License.

package promtest

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Why do we duplicate the e2e test from "internal/promqle2etest"?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Is this rename necessary?

value *monitoring_pb.TypedValue
resetTimestamp int64
)
if entry.metadata.Type == model.MetricTypeHistogram {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We might want to upgrade to this in 3.x

Comment thread google/export/export_bench_test.go Outdated

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Also this

},
},
},
{

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

and this

Comment thread google/secrets/watch.go
secret := e.Object.(*corev1.Secret)
w.s = secret
secret, ok := e.Object.(*corev1.Secret)
if ok && secret != nil && secret.Name == config.Name {

@bwplotka bwplotka Jul 13, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Is this intended? Fixing some particular bug we didn't know of?

If yes - can we have a dedicated PR and test for it?

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.

Not critical, but there was a potential bug/flake detected. AI Summary:

The check secret.Name == config.Name in google/secrets/watch.go was added to solve a critical issue with Kubernetes Mock/Fake clients during unit tests.

The Problem with Fake Clients

Under production conditions, the watcher registers a watch with a FieldSelector limiting events to only the target secret name (fields.OneTermEqualSelector(metav1.ObjectNameField, config.Name)). The real Kubernetes API server ensures we only receive events for that specific secret.

However, in unit tests (defined in kubernetes_test.go), we use Kubernetes' fake.Clientset to mock the API server.

  • The fake clientset's watch implementation does not filter events by field selectors.
  • If a namespace contains multiple secrets (e.g., s1 and s2 in namespace ns1), and a watcher is set up for s1, any updates to s2 will still trigger a watch event inside s1's watcher.

What the Code Accomplishes

If we don't have the secret.Name == config.Name check:

  1. When s2 is updated, the watcher for s1 receives the event.
  2. In the update method, it sets w.s = secret (assigning the s2 secret object to s1's cache).
  3. The next time the application fetches the secret value for s1, it will query the cached s2 object, either returning a wrong value or failing with a key-not-found error.

By adding secret.Name == config.Name in both update and secret, we guarantee that:

  • We ignore any unexpected events from other secrets in the same namespace (primarily during unit testing).
  • We only serve the secret value if the cached secret name actually matches the name of the secret we configured.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Great, do you mind asking AI to generate test for this then?

TAG=agy
CONV=2002b781-f79e-47f1-823f-25677b6b5cb8
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.

2 participants