Skip to content

fix(collector): Don't split metric exports across batches#307

Open
Amund211 wants to merge 1 commit into
mainfrom
collector/dont-split-metric-batches
Open

fix(collector): Don't split metric exports across batches#307
Amund211 wants to merge 1 commit into
mainfrom
collector/dont-split-metric-batches

Conversation

@Amund211

Copy link
Copy Markdown
Owner

Problem

The collector sidecar on flashlight-cr was logging Exporting failed. Dropping data. from the googlemanagedprometheus exporter at a dead-steady ~10/min (1,748 of 1,760 export failures in a 3h window). GMP rejected the two exporter-generated info gauges — target_info and otel_scope_info — with:

code = InvalidArgument desc = One or more TimeSeries could not be written: ...
One or more points were written more frequently than the maximum sampling
period configured for the metric.
{Metric: prometheus.googleapis.com/otel_scope_info/gauge,
 Timestamps: {Youngest Existing: '...:49:51.913', New: '...:49:51.915'}}

i.e. the same info series written ~2ms apart.

Root cause

The metrics pipeline's batch processor used send_batch_max_size: 200 (copied from GCP's Cloud Run OTel sidecar tutorial). That splits each ~60s export into ceil(series / 200) separate batches. The googlemanagedprometheus exporter emits target_info / otel_scope_info once per PushMetrics call, so every extra batch re-writes those series with near-identical timestamps → GMP rejects the duplicates.

The cap is redundant: the exporter already chunks CreateTimeSeries to GCM's hard limit of 200 timeseries/request internally (sendBatchSize = 200 in opentelemetry-operations-go), and target_info is written once per PushMetrics call regardless of series count. So send_batch_max_size: 200 bought nothing except this bug.

⚠️ Likely trigger — verify before merging

We were almost certainly seeing this because we were pushing a lot of high-cardinality labels (user_agent, ip_hash) that blew ports_request_count_total past the OTel SDK's 2000-series cap. At ~2000 series each export split into ~10 batches → the observed steady ~10 rejections/min.

#306 removed those labels. With series back to dozens, each export now fits in a single 200-point batch and target_info is written once per cycle — so the error should stop on its own without this change.

This PR is therefore defense-in-depth, not the primary fix: it makes the failure structurally impossible regardless of future cardinality. We should confirm the error stays gone now that #306 is deployed, and only merge this once we've seen it calm down — that way we're merging a durability guardrail, not papering over an unverified root cause.

(Early signal: revision flashlight-cr-00275-2g2 with #306 served 26,496 requests in 90 min with zero export failures of any kind; the last failure came from the prior revision seconds before cutover. Looks resolved — worth watching for longer before merging.)

Change

Give the metrics pipeline its own batch/metrics processor without send_batch_max_size, so a single export is never split into multiple PushMetrics calls. The traces pipeline keeps the original batch processor unchanged.

   batch:
-    # batch metrics before sending to reduce API usage
+    # Traces pipeline: cap request size. Metrics use batch/metrics below.
     send_batch_max_size: 200
     send_batch_size: 200
     timeout: 5s
+
+  batch/metrics:            # no send_batch_max_size -> export never split
+    send_batch_size: 200
+    timeout: 5s

Debugging methodology

  • Cadencegcloud logging read '... "maximum sampling period"' showed a dead-steady 10/min, not bursty and not correlated with cold starts, with no retry / sending_queue / Permanent error log lines → not a retry-amplification loop.
  • Arithmetic2000 live series / send_batch_size 200 = 10 batches per export, at 1 export/min = the observed 10 errors/min. Exact match.
  • Post-fix: Drop high-cardinality attributes from request metrics #306 check — new revision flashlight-cr-00275-2g2 served 26,496 requests in 90 min with zero export failures; last failure predates cutover.
  • Source readopentelemetry-operations-go collector/metrics.go PushMetrics: target_info is appended once per resource via the ExtraMetrics hook, then the flat timeseries list is chunked into sendBatchSize = 200 slices → target_info lands in exactly one CreateTimeSeries request per PushMetrics call. Splitting upstream is the only way to duplicate it.

Verification

  • collector/config.yaml parses as valid YAML; pipeline wiring confirmed (metricsbatch/metrics, tracesbatch).
  • Could not run otelcol validate locally — the otelcol-google:0.154.0 image registry is outside the sandbox network allowlist. The change is standard OTel config structure.

Sources

🤖 Generated with Claude Code

The metrics pipeline's batch processor used send_batch_max_size: 200
(copied from GCP's Cloud Run OTel sidecar tutorial). That split each ~60s
export into ceil(series / 200) separate batches. The
googlemanagedprometheus exporter emits the target_info / otel_scope_info
info series once per PushMetrics call, so every extra batch re-wrote those
series with near-identical timestamps, and Google Managed Prometheus
rejected the duplicates with:

  code = InvalidArgument desc = One or more TimeSeries could not be written:
  ... One or more points were written more frequently than the maximum
  sampling period configured for the metric.

The cap is redundant: the exporter already chunks CreateTimeSeries to GCM's
hard limit of 200 timeseries/request internally (sendBatchSize = 200 in
opentelemetry-operations-go), and it writes target_info once per
PushMetrics call regardless of series count. Moving metrics to a dedicated
batch/metrics processor with no send_batch_max_size keeps each export in a
single PushMetrics call, so the info series are written exactly once no
matter how high cardinality goes. The traces pipeline keeps the original
batch processor unchanged.

Likely trigger: we were attaching high-cardinality labels (user_agent,
ip_hash) that pushed ports_request_count_total past the OTel SDK's 2000
series cap, so each export split into ~10 batches -> a dead-steady
~10 rejections/min. PR #306 dropped those labels; with series back to
dozens each export fits in one 200-point batch and the error should stop on
its own. This change is defense-in-depth so the failure cannot silently
return if cardinality later creeps back over 200. We should confirm the
error stays gone after #306 has been deployed for a while before merging
this (see methodology below).

Debugging methodology:
- Cadence: gcloud logging read '... "maximum sampling period"' showed a
  dead-steady 10/min, not bursty and not correlated with cold starts, with
  no retry / sending_queue log lines -> not a retry-amplification loop.
- Arithmetic: 2000 live series / send_batch_size 200 = 10 batches per
  export at 1 export/min = the observed 10 errors/min.
- Post-#306 check: revision flashlight-cr-00275-2g2 served 26,496 requests
  in 90 min with zero export failures of any kind; the last failure came
  from the prior revision seconds before cutover.
- Source read: opentelemetry-operations-go collector/metrics.go PushMetrics
  adds target_info once per resource via the ExtraMetrics hook, then chunks
  the flat timeseries list into sendBatchSize=200 slices -> target_info
  lands in exactly one CreateTimeSeries request per PushMetrics call.

Sources:
- https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/main/exporter/collector/metrics.go
- https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/exporter/googlemanagedprometheusexporter/README.md
- open-telemetry/opentelemetry-collector-contrib#31507
- https://github.com/open-telemetry/opentelemetry-collector/blob/main/processor/batchprocessor/README.md
- https://docs.cloud.google.com/stackdriver/docs/instrumentation/opentelemetry-collector-cloud-run

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 11, 2026 23:22

Copilot AI 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.

Pull request overview

Adjusts the OpenTelemetry Collector sidecar configuration to prevent Google Managed Prometheus export rejections caused by the exporter re-emitting info series (target_info, otel_scope_info) when upstream batching splits a single metrics export into multiple PushMetrics calls.

Changes:

  • Keeps the existing batch processor (with send_batch_max_size) for the traces pipeline.
  • Introduces a dedicated batch/metrics processor without send_batch_max_size for the metrics pipeline to avoid upstream split-batching.
  • Rewires the metrics pipeline to use batch/metrics while leaving the rest of the pipeline unchanged.

Comment thread collector/config.yaml
send_batch_size: 200
timeout: 5s

# Metrics pipeline: batch WITHOUT send_batch_max_size (0 = no upper limit).
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