Introduce config parameter limit label expansion - #261
Conversation
c8accbc to
243d3cc
Compare
There was a problem hiding this comment.
Seems that once a record has raised an exception, valid records sent afterwards no longer show up in the metric.
Please try attached file to reproduce.
repro-261.tar.gz
There was a problem hiding this comment.
Pull request overview
Introduces configurable safeguards to bound Prometheus label cardinality and label value growth in filter_prometheus and out_prometheus, mitigating cardinality-driven memory exhaustion risks.
Changes:
- Add
max_label_value_length(truncate label values) andmax_series_per_metric(drop new label sets beyond a cap) with per-<metric>overrides. - Add shared
LogThrottleand use it to throttle repeated “dropped label set” warnings (and refactorin_prometheuserror throttling to use it). - Add/extend specs and documentation for the new limiting behavior.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/fluent/plugin/prometheus/log_throttle_spec.rb | Adds unit tests for the new shared log throttling utility. |
| spec/fluent/plugin/filter_prometheus_spec.rb | Adds coverage for max_series_per_metric behavior and throttled warning logging in the filter plugin. |
| README.md | Documents new label expansion limiting parameters and behavior details. |
| lib/fluent/plugin/prometheus.rb | Implements label truncation, series limiting, label-set limit warnings, and shared LogThrottle. |
| lib/fluent/plugin/out_prometheus.rb | Passes plugin-level metric limit options into metric construction. |
| lib/fluent/plugin/in_prometheus.rb | Replaces bespoke throttling with shared LogThrottle. |
| lib/fluent/plugin/filter_prometheus.rb | Passes plugin-level metric limit options into metric construction. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Both limits are on by default, so upgrading changes the exported metrics of every existing user — and the change is silent
Truncation merges label sets that used to be distinct Verified with the default configuration (no limit set anywhere): <filter test.**>
@type prometheus
<metric>
name test_truncated
type counter
desc test
key val
<labels>
path $.path
</labels>
</metric>
</filter>Feeding two records whose Label values longer than 256 characters are not exotic — URLs with query strings, Kubernetes annotations, SQL statements and exception messages all reach that length routinely. For those users, upgrading makes existing series disappear and a new merged series appear in their place. Prometheus sees the old series go stale, so recording rules, dashboards and alerts built on them break, and the counter values are wrong rather than merely missing. The cap drops records once a metric is saturated A deployment that legitimately runs above 10000 label sets today starts losing everything past the 10001st after the upgrade, with no configuration change on their side. Suggestion Please consider defaulting both to A dropped label set leaves almost no traceThis is what makes the previous point serious: when a record is dropped, there is essentially no way for an operator to find out.
In an earlier reproduction of a related problem, 161 dropped records produced exactly one log line while every Suggestion Self-instrument the drops so they are visible in Prometheus itself rather than only in logs — for example a counter such as If both limits are going to stay on by default, this feels like a prerequisite rather than a nice-to-have: the defaults are what make the loss possible, and this is what would make it noticeable. This comment was written by Claude (Claude Code). The behaviour described above was verified by running the plugin at 39a9ae3. |
39a9ae3 to
b764674
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (3)
lib/fluent/plugin/prometheus.rb:485
release_seriesdeletes the entry unconditionally, which can under-count series when another thread successfully instrumented the same label set while this (reserved) call failed. That can allow subsequent new label sets to pass the@series.sizecheck and exceedmax_series_per_metric.
If you adopt a reserved/confirmed state (e.g. :reserved vs :confirmed), only delete when the label set is still reserved, and update the comment above this method (it currently describes the unsafe behavior as intended).
def release_series(label)
@series_mutex.synchronize do
@series.delete(label)
end
end
lib/fluent/plugin/prometheus.rb:408
- A failing instrumentation can free a series slot that another concurrent instrumentation of the same new label set has effectively “confirmed” by succeeding. This happens because the label set is reserved before yielding, but nothing marks it as confirmed on success, so
release_seriescannot distinguish an in-flight reservation from an established series.
Confirm the label set after a successful yield, so release_series can safely decide whether deletion is still allowed.
reserved = reserve_series!(label)
begin
yield label
rescue
release_series(label) if reserved
lib/fluent/plugin/prometheus.rb:473
reserve_series!records a newly taken slot astrue. To makerelease_seriessafe under concurrent instrumentation of the same label set (one success + one failure), store a distinct reserved state (e.g.:reserved) and have successful instrumentations mark it confirmed.
@series[label] = true
next true
end
In the previous versions, there is no mechanism to limit label expansion. That causes a possibility of cardinality OOM DoS. To mitigate such situation, introduced the following parameters for filter_prometheus and out_prometheus: * max_label_value_length: The maximum length of a label value. * max_series_per_metric: The maximum number of label sets a metric can hold. For example, if about 8 million records are loaded without cardinality limitation, RSS increased from 64MB to 582MB. It might cause OOM DoS. In contrast to that case with cardinality limitation, RSS increased from 64MB to 84 MB in similar case. Additionally the following counter is introduced too. * fluentd_prometheus_dropped_label_sets_total About internal design: Both limits are unlimited by default and have to be opted in. Enabling them changes what an existing metric exposes without telling anyone: a truncated label value merges label sets which were distinct so far and sums their values, and a dropped label set loses the record. Label values above a few hundred characters are not exotic, and a deployment which legitimately runs above 10000 label sets would start losing records right after an upgrade. That is a decision for the operator who knows the records, so each limit can be overridden per <metric> as well. A metric is instrumented through with_label_set, which takes the slot before instrumenting instead of counting the label set afterwards: two threads which build a new label set at the same time would otherwise both pass the check and expand the metric beyond max_series_per_metric. The slot is taken as :reserved and marked :confirmed once the client actually holds the label set, so that a record which fails to be instrumented, for example when the value of `key` is not a number, gives its own reservation back without dropping a series a concurrent record established in the meantime. Records which fail that way must not consume the limit, otherwise they could exhaust it and make the following valid label sets dropped. A drop is hard to notice, since it is not routed to @error and the warning is throttled by ignore_error_log_interval, which both plugins now take as well. Count the drops in a new fluentd_prometheus_dropped_label_sets_total counter, labelled with the metric name and registered on the first drop, so that an operator can alert on a metric which is losing records. The throttling itself is extracted into LogThrottle, which in_prometheus shares. Then warning message is logged like this: 2026-08-06 14:06:04 +0900 [warn]: prometheus: dropped a label set because the metric reached max_series_per_metric. name="access_requests_total" max_series_per_metric=10000 Signed-off-by: Kentaro Hayashi <hayashi@clear-code.com> Co-Authored-By: Claude <noreply@anthropic.com>
max_series_per_metric refuses a label set once a metric is full, max_label_value_length truncates a label value, and the warning about a dropped label set is throttled by LogThrottle, which in_prometheus shares. in_prometheus_spec used to cover the throttling logic itself, from before LogThrottle was extracted. Some test cases were removed from there. Keep there only what a unit spec cannot reach: which slot a failure is throttled on, how a fingerprint is built out of an exception, the suppressed_log_count the log carries, and the 500 the client keeps receiving meanwhile. Signed-off-by: Kentaro Hayashi <hayashi@clear-code.com> Co-Authored-By: Claude <noreply@anthropic.com>
b764674 to
7207697
Compare
In the previous versions, there is no mechanism to limit label expansion. That causes a possibility of cardinality OOM DoS.
To mitigate such situation, introduced the following parameters:
The above parameter is configurable for filter_prometheus and out_prometheus.
For example, if about 8 million records are loaded without cardinality
limitation, RSS increased from 64MB to 582MB. It might cause OOM DoS.
In contrast to that case with cardinality limitation, RSS increased
from 64MB to 84 MB in similar case.
Additionally the following counter is introduced too.