fix(collector): Don't split metric exports across batches#307
Open
Amund211 wants to merge 1 commit into
Open
Conversation
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>
There was a problem hiding this comment.
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
batchprocessor (withsend_batch_max_size) for the traces pipeline. - Introduces a dedicated
batch/metricsprocessor withoutsend_batch_max_sizefor the metrics pipeline to avoid upstream split-batching. - Rewires the metrics pipeline to use
batch/metricswhile leaving the rest of the pipeline unchanged.
| send_batch_size: 200 | ||
| timeout: 5s | ||
|
|
||
| # Metrics pipeline: batch WITHOUT send_batch_max_size (0 = no upper limit). |
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
The
collectorsidecar onflashlight-crwas loggingExporting failed. Dropping data.from thegooglemanagedprometheusexporter 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_infoandotel_scope_info— with:i.e. the same info series written ~2ms apart.
Root cause
The metrics pipeline's
batchprocessor usedsend_batch_max_size: 200(copied from GCP's Cloud Run OTel sidecar tutorial). That splits each ~60s export intoceil(series / 200)separate batches. Thegooglemanagedprometheusexporter emitstarget_info/otel_scope_infoonce perPushMetricscall, so every extra batch re-writes those series with near-identical timestamps → GMP rejects the duplicates.The cap is redundant: the exporter already chunks
CreateTimeSeriesto GCM's hard limit of 200 timeseries/request internally (sendBatchSize = 200inopentelemetry-operations-go), andtarget_infois written once perPushMetricscall regardless of series count. Sosend_batch_max_size: 200bought nothing except this bug.We were almost certainly seeing this because we were pushing a lot of high-cardinality labels (
user_agent,ip_hash) that blewports_request_count_totalpast 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_infois 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-2g2with #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/metricsprocessor withoutsend_batch_max_size, so a single export is never split into multiplePushMetricscalls. The traces pipeline keeps the originalbatchprocessor unchanged.Debugging methodology
gcloud logging read '... "maximum sampling period"'showed a dead-steady 10/min, not bursty and not correlated with cold starts, with noretry/sending_queue/Permanent errorlog lines → not a retry-amplification loop.2000 live series / send_batch_size 200 = 10 batches per export, at 1 export/min = the observed 10 errors/min. Exact match.flashlight-cr-00275-2g2served 26,496 requests in 90 min with zero export failures; last failure predates cutover.opentelemetry-operations-gocollector/metrics.goPushMetrics:target_infois appended once per resource via theExtraMetricshook, then the flat timeseries list is chunked intosendBatchSize = 200slices →target_infolands in exactly oneCreateTimeSeriesrequest perPushMetricscall. Splitting upstream is the only way to duplicate it.Verification
collector/config.yamlparses as valid YAML; pipeline wiring confirmed (metrics→batch/metrics,traces→batch).otelcol validatelocally — theotelcol-google:0.154.0image registry is outside the sandbox network allowlist. The change is standard OTel config structure.Sources
opentelemetry-operations-gocollector/metrics.go—PushMetrics,sendBatchSize = 200,ExtraMetrics/target_infogooglemanagedprometheusexporterREADME —enable_target_info/enable_scope_infosend_batch_max_sizesplitting semanticssend_batch_max_size: 200config🤖 Generated with Claude Code