From 396ec8f5d2aeda24892c1dc0c9e5b8cbfde4cbac Mon Sep 17 00:00:00 2001 From: Kentaro Hayashi Date: Wed, 26 Aug 2026 02:42:23 +0000 Subject: [PATCH] Refuse which cannot fit into max_series_per_metric label sets take their slots at startup, so a limit smaller than their number is already exceeded before any record arrives: the metric could never take a new label set. Stop at startup instead. A record on one of those label sets is still counted, since the metric already holds it. A limit equal to the number of label sets is fine: every label set is known in advance. Co-Authored-By: Claude Signed-off-by: Kentaro Hayashi --- README.md | 6 ++++ lib/fluent/plugin/prometheus.rb | 16 ++++++++++ .../plugin/prometheus/series_limit_spec.rb | 29 +++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/README.md b/README.md index 0d0b921..b278849 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,12 @@ not a number, does not consume the limit. A pre-initialized label set (`initialized` and ``) consumes it from the start, since the metric holds it before any record arrives. +A `` section is refused at startup with a configuration error when its +limit is smaller than the number of its own `` label sets: the limit +is already exceeded before any record arrives, so the metric could never take a +new label set. A limit equal to that number is fine, since every label set of +the metric is known in advance. + ##### Observing what the limit leaves out A dropped label set is not routed to `@ERROR`, because it is what the diff --git a/lib/fluent/plugin/prometheus.rb b/lib/fluent/plugin/prometheus.rb index 87d1e5c..ae64c2d 100644 --- a/lib/fluent/plugin/prometheus.rb +++ b/lib/fluent/plugin/prometheus.rb @@ -460,9 +460,25 @@ def bind_series_set(client_metric) @base_initlabels.each do |initlabels| confirm_series(normalize_label_set(initlabels)) end + check_initlabels_fit_series_limit! end end + # The client is given these label sets at startup, so a limit which + # does not fit them is already exceeded before any record arrives and + # the metric could never take a new one. Stop instead of running that + # way. A record on one of them is still counted, since the metric + # already holds its label set. + def check_initlabels_fit_series_limit! + return if @max_series_per_metric <= 0 + # two blocks with the same values make one label set + return if @series_set.size <= @max_series_per_metric + + raise ConfigError, "metric #{@name} holds #{@series_set.size} label sets from , " \ + "but max_series_per_metric is #{@max_series_per_metric}: " \ + "the limit is already exceeded before any record arrives" + end + # The SeriesSet keys a label set by its values, so the same value has to # look the same whether a RecordAccessor or produced it. def normalize_label_value(value) diff --git a/spec/fluent/plugin/prometheus/series_limit_spec.rb b/spec/fluent/plugin/prometheus/series_limit_spec.rb index 7cd9d63..2172d6e 100644 --- a/spec/fluent/plugin/prometheus/series_limit_spec.rb +++ b/spec/fluent/plugin/prometheus/series_limit_spec.rb @@ -270,6 +270,16 @@ def instrument_other(path, value = 1) ) end + context 'with a limit below the number of label sets' do + # 3 label sets exist at startup, so a limit of 1 would drop every record + let(:max_series_per_metric) { 1 } + + it 'stops at startup instead of dropping every record' do + expect { metric }.to raise_error(Fluent::ConfigError, + /holds 3 label sets from .*max_series_per_metric is 1/) + end + end + context 'with a limit equal to the number of label sets' do # every label set is known in advance, so the limit is reached but no # record is dropped @@ -289,6 +299,25 @@ def instrument_other(path, value = 1) expect { instrument('/d') }.to raise_error(Fluent::Plugin::Prometheus::LabelSetLimitError) end end + + context 'with two holding the same values' do + # both make the same label set, so they take one slot + let(:initlabels) { ['/a', '/a'] } + let(:max_series_per_metric) { 1 } + + it 'counts the label sets and not the blocks' do + expect { metric }.not_to raise_error + end + end + + context 'without a limit' do + let(:max_series_per_metric) { 0 } + + it 'accepts any number of label sets' do + expect { metric }.not_to raise_error + expect { instrument('/d') }.not_to raise_error + end + end end describe ' overriding the plugin limit' do