fix: harden metric scrape edge cases#2297
Conversation
|
6cc0f6c to
9a7bf86
Compare
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
9a7bf86 to
0020301
Compare
There was a problem hiding this comment.
Pull request overview
This PR hardens the metrics scrape path across core buffering, query parsing/filtering, and the standalone HTTPServer exporter to address multiple reported scrape-triggered crashes/DoS vectors and information disclosure.
Changes:
- Make
Bufferthread striping overflow-safe and bound spin-wait during collection (replaying buffered observations before failing). - Stop exposing stack traces to HTTP clients; return a generic 500 error body while logging server-side.
- Bound query parsing (length + parameter count) and de-duplicate metric-name filters for more predictable scrape cost.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/MetricNameFilter.java | De-duplicates filter inputs and adds fast-path contains() checks. |
| prometheus-metrics-exporter-httpserver/src/test/java/io/prometheus/metrics/exporter/httpserver/HTTPServerTest.java | Updates tests for new default executor + generic error responses. |
| prometheus-metrics-exporter-httpserver/src/test/java/io/prometheus/metrics/exporter/httpserver/HttpExchangeAdapterTest.java | Verifies generic 500 body and absence of stack traces. |
| prometheus-metrics-exporter-httpserver/src/main/java/io/prometheus/metrics/exporter/httpserver/HTTPServer.java | Switches default executor to bounded queue and changes unauthorized-body handling. |
| prometheus-metrics-exporter-httpserver/src/main/java/io/prometheus/metrics/exporter/httpserver/HttpExchangeAdapter.java | Replaces stack-trace responses with a generic error message + server-side logging. |
| prometheus-metrics-exporter-httpserver/src/main/java/io/prometheus/metrics/exporter/httpserver/BlockingRejectedExecutionHandler.java | Removes the blocking rejection handler to avoid dispatcher thread stalls. |
| prometheus-metrics-exporter-common/src/test/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandlerTest.java | Adds tests for rejecting overly-long / overly-many query parameters. |
| prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandler.java | Catches invalid query parameter parsing and returns HTTP 400. |
| prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusHttpRequest.java | Implements bounded query parsing for getParameterValues(). |
| prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/InvalidQueryParameterException.java | Introduces an internal exception for invalid query parsing. |
| prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/BufferTest.java | Adds regression tests for stripe indexing and timeout/replay behavior. |
| prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Buffer.java | Uses floorMod for striping; adds spin-wait timeout + replay-on-timeout. |
| prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/UtilTest.java | Updates tests for redacted/escaped invalid-value error messages. |
| prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/OpenMetrics2PropertiesTest.java | Updates expected messages to quote/escape invalid values. |
| prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPushgatewayPropertiesTest.java | Updates expected messages to quote/escape invalid values. |
| prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPropertiesTest.java | Updates expected messages to quote/escape invalid values. |
| prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java | Centralizes invalid-value message formatting with escaping + truncation. |
| prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterPushgatewayProperties.java | Uses Util.invalidValueMessage(...) to avoid leaking raw config values. |
| integration-tests/it-exporter/it-exporter-test/src/test/java/io/prometheus/metrics/it/exporter/test/HttpServerIT.java | Validates generic HTTPServer error body in integration tests. |
| integration-tests/it-exporter/it-exporter-test/src/test/java/io/prometheus/metrics/it/exporter/test/ExporterIT.java | Factors error-body assertions behind an overridable hook. |
| docs/apidiffs/current_vs_latest/prometheus-metrics-exporter-httpserver.txt | Records API-diff output for exporter-httpserver changes. |
| docs/apidiffs/current_vs_latest/prometheus-metrics-exporter-common.txt | Records API-diff output for exporter-common changes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
| observeFunction.accept(buffer[i]); | ||
| } | ||
| if (timedOut) { | ||
| throw new IllegalStateException("Timed out while waiting for in-flight observations."); |
There was a problem hiding this comment.
Throwing here is the intended behavior on the scrape path (Histogram.collect → buffer.run), but run() is also invoked from maybeResetOrScaleDown() / maybeScaleDown() (Histogram.java:439, :470), which sit on the application recording path: observe → doObserve → maybeResetOrScaleDown → buffer.run. Before this change observe() could never throw from here. Under sustained contention or a GC pause > 1 s where in-flight fast-path observers don't settle before the deadline, a native-histogram observe() will now throw an undocumented IllegalStateException into user code — a new failure mode close to the crash class #2282 fixes.
On the internal reset/scale-down path, should we log and defer (skip this cycle's scale-down) instead of throwing, and then only surface the timeout on the scrape/collect() path?
There was a problem hiding this comment.
Fixed in ec7f83d. Histogram.collect still calls run with timeout failure enabled, while the reset/scale-down paths call it with failOnTimeout=false. A timeout during maintenance now deactivates and drains the generation, then skips that maintenance cycle instead of throwing into observe(). I also added regression coverage for the timeout/replay behavior.
| } | ||
| } | ||
|
|
||
| static String invalidValueMessage(String fullKey, String message, String found) { |
There was a problem hiding this comment.
Escaping control chars (log-injection / CWE-117 defense) and the 100-char cap are good, but the raw value stays in the message. The client-facing leak via HttpExchangeAdapter is closed by the #2283 fix; however the servlet adapters (servlet-jakarta / javax) re-throw to the container, so up to 100 chars of the offending config value can still land in a container error page.
There was a problem hiding this comment.
Fixed in ec7f83d. Invalid configuration messages now include only the property key and expected format; the offending raw value is no longer included. This is also applied to the PushGateway credential-related paths, so servlet/container error propagation cannot expose the configured value.
| return (int) Math.floorMod(threadId, stripeCount); | ||
| } | ||
|
|
||
| private void doAppend(double amount) { |
There was a problem hiding this comment.
observationBuffer still grows unbounded; the cap is now only implicit via the 1 s active window. Reasonable, but a hard MAX_BUFFER_SIZE with drop-on-overflow would make the bound explicit and independent of the timeout value.
There was a problem hiding this comment.
Addressed in ec7f83d. The observation generation now has an explicit DEFAULT_MAX_BUFFER_SIZE of 1,000,000, independent of the timeout. Appends wait for space while the generation is active and stop when it is deactivated; I chose bounded blocking rather than drop-on-overflow so recording does not silently lose observations.
| Thread.yield(); | ||
| } | ||
| result = createResult.get(); | ||
| result = timedOut ? null : createResult.get(); |
There was a problem hiding this comment.
The spin-wait timeout covers this first loop, but the second wait loop below (while (bufferPos < expectedBufferSize) bufferFilled.await(), ~line 137) has no timeout: a thread stalled between the stripe increment in append() and doAppend() can still block run() there indefinitely. Narrow window, but the "hang forever" class isn't fully eliminated — consider a bounded await there too.
There was a problem hiding this comment.
Fixed in ec7f83d. The buffer handoff is now coordinated through generation/phase transitions, so the second unbounded bufferFilled.await() is gone. Appenders that straddle activation/deactivation are accounted for, and there is regression coverage for a stalled appender.
| this(DEFAULT_MAX_SPIN_WAIT_NANOS); | ||
| } | ||
|
|
||
| Buffer(long maxSpinWaitNanos) { |
There was a problem hiding this comment.
The 1 s deadline is only reachable via this package-private constructor (tests). Should we expose a knob for this?
There was a problem hiding this comment.
The deadline constructor remains package-private and test-only; there is no public configuration knob. The production constructor continues to use the default one-second deadline.
| responseSent = true; | ||
| logger.log( | ||
| Level.SEVERE, | ||
| "The Prometheus metrics HTTPServer caught an Exception during scrape.", |
There was a problem hiding this comment.
This reverses the prior "avoid logging in Java-agent mode" behavior — correct, since the client no longer gets details. But a persistent misconfiguration will emit a SEVERE line on every scrape. Maybe we should add rate-limiting/deduping the log to avoid flooding logs.
There was a problem hiding this comment.
Agreed. I left rate limiting/deduplication as a follow-up rather than expanding this change's scope. This PR removes exception details from the client response and logs the exception server-side, but a persistent misconfiguration can still produce one SEVERE entry per failing scrape. I am leaving this thread unresolved.
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
|
Closing this combined PR because its long review history makes the individual fixes difficult to evaluate. I’ll replace it with focused PRs for the separate reports, and each PR will call out any discussion that is still ongoing. |
Summary
This addresses the bug reports from #2282, #2283, #2284, #2285, #2286, and #2287 in one PR.
Bufferstripe indexing so large thread IDs cannot produce negative stripe indexes.Bufferspin wait during collection and replay buffered observations before failing the scrape on timeout.PrometheusPropertiesExceptionmessages.Fixes #2282
Fixes #2283
Fixes #2284
Fixes #2285
Fixes #2286
Fixes #2287
Validation
mise run lint:fix./mvnw -pl prometheus-metrics-core,prometheus-metrics-config,prometheus-metrics-exporter-common,prometheus-metrics-exporter-httpserver,prometheus-metrics-model test -Dcoverage.skip=true -Dcheckstyle.skip=truemise run build