fluent-plugin-prometheus 2.2.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/bug_report.yaml +72 -0
- data/.github/ISSUE_TEMPLATE/config.yml +5 -0
- data/.github/ISSUE_TEMPLATE/feature_request.yaml +38 -0
- data/.github/dependabot.yml +18 -1
- data/.github/workflows/add-to-project.yml +24 -0
- data/.github/workflows/linux.yml +13 -3
- data/.gitignore +1 -0
- data/ChangeLog +17 -0
- data/README.md +129 -2
- data/fluent-plugin-prometheus.gemspec +1 -1
- data/lib/fluent/plugin/filter_prometheus.rb +1 -1
- data/lib/fluent/plugin/in_prometheus/async_wrapper.rb +9 -2
- data/lib/fluent/plugin/in_prometheus.rb +73 -24
- data/lib/fluent/plugin/out_prometheus.rb +3 -1
- data/lib/fluent/plugin/prometheus/log_throttle.rb +45 -0
- data/lib/fluent/plugin/prometheus/placeholder_expander.rb +15 -1
- data/lib/fluent/plugin/prometheus.rb +299 -15
- data/spec/fluent/plugin/filter_prometheus_spec.rb +141 -0
- data/spec/fluent/plugin/in_prometheus_spec.rb +257 -1
- data/spec/fluent/plugin/out_prometheus_spec.rb +29 -1
- data/spec/fluent/plugin/prometheus/log_throttle_spec.rb +127 -0
- data/spec/fluent/plugin/prometheus/placeholder_expander_spec.rb +96 -0
- data/spec/fluent/plugin/prometheus/series_limit_spec.rb +469 -0
- data/spec/fluent/plugin/shared.rb +113 -1
- data/spec/spec_helper.rb +18 -0
- metadata +12 -6
|
@@ -50,6 +50,94 @@ describe Fluent::Plugin::Prometheus::ExpandBuilder::PlaceholderExpander do
|
|
|
50
50
|
expander.expand('${hostname}')
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# tag_parts, tag_prefix and tag_suffix must be expanded with the tag,
|
|
54
|
+
# not with "forged"
|
|
55
|
+
context 'with a value named after a tag placeholder' do
|
|
56
|
+
let(:forged_placeholder) do
|
|
57
|
+
{
|
|
58
|
+
'tag' => '1.2.3',
|
|
59
|
+
'tag_parts' => %w[forged forged forged],
|
|
60
|
+
'tag_prefix' => %w[forged forged forged],
|
|
61
|
+
'tag_suffix' => %w[forged forged forged],
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'expands the placeholders with the tag' do
|
|
66
|
+
expander = builder.build(forged_placeholder)
|
|
67
|
+
|
|
68
|
+
expect(expander.expand('${tag_parts[0]}.${tag_parts[1]}.${tag_parts[2]}')).to eq('1.2.3')
|
|
69
|
+
expect(expander.expand('${tag_parts[-3]}.${tag_parts[-2]}.${tag_parts[-1]}')).to eq('1.2.3')
|
|
70
|
+
expect(expander.expand('${tag_prefix[0]},${tag_prefix[1]},${tag_prefix[2]}')).to eq('1,1.2,1.2.3')
|
|
71
|
+
expect(expander.expand('${tag_suffix[0]},${tag_suffix[1]},${tag_suffix[2]}')).to eq('3,2.3,1.2.3')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'does not expand an index which the tag does not have' do
|
|
75
|
+
# tag_prefix and tag_suffix have no negative index, so they are kept
|
|
76
|
+
# as they are. This checks that the record does not fill them.
|
|
77
|
+
expander = builder.build(forged_placeholder)
|
|
78
|
+
|
|
79
|
+
expect(expander.expand('${tag_prefix[-1]}')).to eq('${tag_prefix[-1]}')
|
|
80
|
+
expect(expander.expand('${tag_suffix[-1]}')).to eq('${tag_suffix[-1]}')
|
|
81
|
+
|
|
82
|
+
# the tag has 3 parts, so the 4th value of the record is out of it.
|
|
83
|
+
longer = forged_placeholder.merge('tag_parts' => %w[forged forged forged forged])
|
|
84
|
+
expander = builder.build(longer)
|
|
85
|
+
|
|
86
|
+
expect(expander.expand('${tag_parts[3]}')).to eq('${tag_parts[3]}')
|
|
87
|
+
expect(expander.expand('${tag_parts[-4]}')).to eq('${tag_parts[-4]}')
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# a record may also have a key named "tag_parts[0]", which makes the
|
|
92
|
+
# same placeholder as the tag
|
|
93
|
+
context 'with a value whose key is a tag placeholder' do
|
|
94
|
+
it 'expands the placeholders with the tag' do
|
|
95
|
+
spelled = {
|
|
96
|
+
'tag' => '1.2.3',
|
|
97
|
+
'tag_parts[0]' => 'forged',
|
|
98
|
+
'tag_prefix[0]' => 'forged',
|
|
99
|
+
'tag_suffix[0]' => 'forged',
|
|
100
|
+
}
|
|
101
|
+
expander = builder.build(spelled)
|
|
102
|
+
|
|
103
|
+
expect(expander.expand('${tag_parts[0]}')).to eq('1')
|
|
104
|
+
expect(expander.expand('${tag_prefix[0]}')).to eq('1')
|
|
105
|
+
expect(expander.expand('${tag_suffix[0]}')).to eq('3')
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'does not expand an index which the tag does not have' do
|
|
109
|
+
spelled = {
|
|
110
|
+
'tag' => '1',
|
|
111
|
+
'tag_parts[1]' => 'forged',
|
|
112
|
+
'tag_parts[-2]' => 'forged',
|
|
113
|
+
'tag_prefix[-1]' => 'forged',
|
|
114
|
+
'tag_suffix[-1]' => 'forged',
|
|
115
|
+
}
|
|
116
|
+
expander = builder.build(spelled)
|
|
117
|
+
|
|
118
|
+
expect(expander.expand('${tag_parts[1]}')).to eq('${tag_parts[1]}')
|
|
119
|
+
expect(expander.expand('${tag_parts[-2]}')).to eq('${tag_parts[-2]}')
|
|
120
|
+
expect(expander.expand('${tag_prefix[-1]}')).to eq('${tag_prefix[-1]}')
|
|
121
|
+
expect(expander.expand('${tag_suffix[-1]}')).to eq('${tag_suffix[-1]}')
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'keeps the tag itself' do
|
|
125
|
+
spelled = {
|
|
126
|
+
'tag' => '1.2.3',
|
|
127
|
+
'tag_parts' => 'forged',
|
|
128
|
+
'tag_prefix' => 'forged',
|
|
129
|
+
'tag_suffix' => 'forged',
|
|
130
|
+
}
|
|
131
|
+
expander = builder.build(spelled)
|
|
132
|
+
|
|
133
|
+
expect(expander.expand('${tag}')).to eq('1.2.3')
|
|
134
|
+
# these are not built from the tag, so they are kept as they are
|
|
135
|
+
expect(expander.expand('${tag_parts}')).to eq('${tag_parts}')
|
|
136
|
+
expect(expander.expand('${tag_prefix}')).to eq('${tag_prefix}')
|
|
137
|
+
expect(expander.expand('${tag_suffix}')).to eq('${tag_suffix}')
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
53
141
|
context 'when not found placeholder' do
|
|
54
142
|
it 'prints wanring log and as it is' do
|
|
55
143
|
expect(log).to receive(:warn).with('unknown placeholder `${tag_prefix[100]}` found').once
|
|
@@ -87,6 +175,14 @@ describe Fluent::Plugin::Prometheus::ExpandBuilder::PlaceholderExpander do
|
|
|
87
175
|
expect(expander.expand('${tag_suffix[0]}.${tag_suffix[1]}.${tag_suffix[2]}', dynamic_placeholders: dynamic_placeholder)).to eq('3.2.3.1.2.3')
|
|
88
176
|
end
|
|
89
177
|
|
|
178
|
+
it 'expands the placeholders with the dynamic tag' do
|
|
179
|
+
forged = static_placeholder.merge('tag_parts' => %w[forged forged forged])
|
|
180
|
+
expander = builder.build(forged)
|
|
181
|
+
|
|
182
|
+
expect(expander.expand('${tag_parts[0]}.${tag_parts[1]}.${tag_parts[2]}',
|
|
183
|
+
dynamic_placeholders: dynamic_placeholder)).to eq('1.2.3')
|
|
184
|
+
end
|
|
185
|
+
|
|
90
186
|
it 'does not create expander twice if given the same placeholder' do
|
|
91
187
|
builder # cached before mock
|
|
92
188
|
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# The limit is exercised through the plugins as well, by the 'limits label
|
|
4
|
+
# expansion' shared examples. These examples stay at the Metric level, where a
|
|
5
|
+
# slot can be observed while an instrumentation is still running.
|
|
6
|
+
describe Fluent::Plugin::Prometheus::Metric do
|
|
7
|
+
let(:registry) { ::Prometheus::Client::Registry.new }
|
|
8
|
+
let(:max_series_per_metric) { 1 }
|
|
9
|
+
let(:element) do
|
|
10
|
+
Fluent::Config::Element.new(
|
|
11
|
+
'metric', '',
|
|
12
|
+
{
|
|
13
|
+
'name' => 'limited',
|
|
14
|
+
'type' => 'counter',
|
|
15
|
+
'desc' => 'Something foo.',
|
|
16
|
+
'key' => 'foo',
|
|
17
|
+
'max_series_per_metric' => max_series_per_metric.to_s,
|
|
18
|
+
},
|
|
19
|
+
[Fluent::Config::Element.new('labels', '', {'path' => '$.path'}, [])]
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
# the label is a RecordAccessor, so no placeholder is expanded here
|
|
23
|
+
let(:expander) { double('expander') }
|
|
24
|
+
let(:metric) { Fluent::Plugin::Prometheus::Counter.new(element, registry, {}, {}) }
|
|
25
|
+
# the client metric is registered by the Metric, so it has to be built before
|
|
26
|
+
# the registry is asked for it
|
|
27
|
+
let(:client_counter) do
|
|
28
|
+
metric
|
|
29
|
+
registry.get(:limited)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def instrument(path, value = 1)
|
|
33
|
+
metric.instrument({'foo' => value, 'path' => path}, expander)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build_metrics(*elements)
|
|
37
|
+
Fluent::Plugin::Prometheus.parse_metrics_elements(
|
|
38
|
+
Fluent::Config::Element.new('ROOT', '', {}, elements), registry, {}, {}
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# A <metric> section named 'shared', so that two of them instrument the same
|
|
43
|
+
# client metric.
|
|
44
|
+
def counter_element(limit, initlabels = nil)
|
|
45
|
+
attributes = {
|
|
46
|
+
'name' => 'shared',
|
|
47
|
+
'type' => 'counter',
|
|
48
|
+
'desc' => 'Something foo.',
|
|
49
|
+
'key' => 'foo',
|
|
50
|
+
'max_series_per_metric' => limit.to_s,
|
|
51
|
+
}
|
|
52
|
+
attributes['initialized'] = 'true' if initlabels
|
|
53
|
+
|
|
54
|
+
Fluent::Config::Element.new(
|
|
55
|
+
'metric', '', attributes,
|
|
56
|
+
[Fluent::Config::Element.new('labels', '', {'path' => '$.path'}, [])] +
|
|
57
|
+
Array(initlabels).map { |path| Fluent::Config::Element.new('initlabels', '', {'path' => path}, []) }
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe 'max_series_per_metric' do
|
|
62
|
+
it 'refuses a new label set once the limit is reached' do
|
|
63
|
+
instrument('/a')
|
|
64
|
+
|
|
65
|
+
expect { instrument('/b') }.to raise_error(Fluent::Plugin::Prometheus::LabelSetLimitError)
|
|
66
|
+
expect(client_counter.values.keys).to eq([{path: '/a'}])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'gives the slot back when the instrumentation failed' do
|
|
70
|
+
# a negative value makes Counter#increment raise, after the label set has
|
|
71
|
+
# been reserved
|
|
72
|
+
expect { instrument('/a', -1) }.to raise_error(ArgumentError)
|
|
73
|
+
|
|
74
|
+
expect { instrument('/b') }.not_to raise_error
|
|
75
|
+
expect(client_counter.values.keys).to eq([{path: '/b'}])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'does not take a slot for a value which is not a number' do
|
|
79
|
+
# such a value is refused before the label set is reserved, so the metric
|
|
80
|
+
# is left as if the record had never arrived
|
|
81
|
+
expect { instrument('/a', 'not a number') }.to raise_error(ArgumentError)
|
|
82
|
+
|
|
83
|
+
expect { instrument('/b') }.not_to raise_error
|
|
84
|
+
expect(client_counter.values.keys).to eq([{path: '/b'}])
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'takes the slot before instrumenting, so that concurrent calls cannot both pass' do
|
|
88
|
+
# the slot has to be taken under the same lock as the check: taking it
|
|
89
|
+
# after the client call would let both label sets through and expand the
|
|
90
|
+
# metric beyond max_series_per_metric
|
|
91
|
+
instrumenting = Queue.new
|
|
92
|
+
resume = Queue.new
|
|
93
|
+
allow(client_counter).to receive(:increment).and_wrap_original do |original, *args, **kwargs|
|
|
94
|
+
instrumenting << true
|
|
95
|
+
resume.pop
|
|
96
|
+
original.call(*args, **kwargs)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
first = Thread.new { instrument('/a') }
|
|
100
|
+
instrumenting.pop # '/a' is inside the client call and holds the only slot
|
|
101
|
+
|
|
102
|
+
expect { instrument('/b') }.to raise_error(Fluent::Plugin::Prometheus::LabelSetLimitError)
|
|
103
|
+
|
|
104
|
+
resume << true
|
|
105
|
+
first.join
|
|
106
|
+
|
|
107
|
+
expect(client_counter.values.keys).to eq([{path: '/a'}])
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Two records which expand to the very same label set may be instrumented
|
|
111
|
+
# at the same time: only one of them reserves the slot, the other one joins
|
|
112
|
+
# that reservation. Giving the slot back on failure must then not drop a
|
|
113
|
+
# label set the client already holds, otherwise a new one would take its
|
|
114
|
+
# place and the metric would grow past max_series_per_metric.
|
|
115
|
+
context 'when concurrent instrumentations share a label set' do
|
|
116
|
+
# stalls the very first client call, so that a second instrumentation can
|
|
117
|
+
# be run while the first one is still in flight
|
|
118
|
+
def stall_first_instrumentation(entered, resume)
|
|
119
|
+
stalled = false
|
|
120
|
+
allow(client_counter).to receive(:increment).and_wrap_original do |original, *args, **kwargs|
|
|
121
|
+
unless stalled
|
|
122
|
+
stalled = true
|
|
123
|
+
entered << true
|
|
124
|
+
resume.pop
|
|
125
|
+
end
|
|
126
|
+
original.call(*args, **kwargs)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'keeps the slot when the call which reserved it fails after another one succeeded' do
|
|
131
|
+
entered = Queue.new
|
|
132
|
+
resume = Queue.new
|
|
133
|
+
stall_first_instrumentation(entered, resume)
|
|
134
|
+
|
|
135
|
+
# a negative value is the one which fails inside the client call: a
|
|
136
|
+
# value which is not a number never gets there, so it could not be
|
|
137
|
+
# stalled
|
|
138
|
+
failing = Thread.new do
|
|
139
|
+
expect { instrument('/a', -1) }.to raise_error(ArgumentError)
|
|
140
|
+
end
|
|
141
|
+
entered.pop # {path: '/a'} is reserved by the record which is about to fail
|
|
142
|
+
|
|
143
|
+
# joins that reservation and does give the label set to the client
|
|
144
|
+
instrument('/a')
|
|
145
|
+
|
|
146
|
+
resume << true
|
|
147
|
+
failing.join
|
|
148
|
+
|
|
149
|
+
# the client holds {path: '/a'}, so its slot must stay taken
|
|
150
|
+
expect { instrument('/b') }.to raise_error(Fluent::Plugin::Prometheus::LabelSetLimitError)
|
|
151
|
+
expect(client_counter.values.keys).to eq([{path: '/a'}])
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it 'keeps the slot when a call which joined a reservation fails' do
|
|
155
|
+
entered = Queue.new
|
|
156
|
+
resume = Queue.new
|
|
157
|
+
stall_first_instrumentation(entered, resume)
|
|
158
|
+
|
|
159
|
+
pending_call = Thread.new { instrument('/a') }
|
|
160
|
+
entered.pop # {path: '/a'} is reserved and being instrumented
|
|
161
|
+
|
|
162
|
+
# joins that reservation and fails in the client, without owning the slot
|
|
163
|
+
expect { instrument('/a', -1) }.to raise_error(ArgumentError)
|
|
164
|
+
|
|
165
|
+
resume << true
|
|
166
|
+
pending_call.join
|
|
167
|
+
|
|
168
|
+
expect { instrument('/b') }.to raise_error(Fluent::Plugin::Prometheus::LabelSetLimitError)
|
|
169
|
+
expect(client_counter.values.keys).to eq([{path: '/a'}])
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it 'takes the slot back when a joined call succeeds after the reservation was released' do
|
|
173
|
+
failing_entered = Queue.new
|
|
174
|
+
failing_resume = Queue.new
|
|
175
|
+
succeeding_entered = Queue.new
|
|
176
|
+
succeeding_resume = Queue.new
|
|
177
|
+
allow(client_counter).to receive(:increment).and_wrap_original do |original, *args, **kwargs|
|
|
178
|
+
case kwargs[:by]
|
|
179
|
+
when -1
|
|
180
|
+
failing_entered << true
|
|
181
|
+
failing_resume.pop
|
|
182
|
+
when 2
|
|
183
|
+
succeeding_entered << true
|
|
184
|
+
succeeding_resume.pop
|
|
185
|
+
end
|
|
186
|
+
original.call(*args, **kwargs)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
failing = Thread.new do
|
|
190
|
+
expect { instrument('/a', -1) }.to raise_error(ArgumentError)
|
|
191
|
+
end
|
|
192
|
+
failing_entered.pop # {path: '/a'} is reserved
|
|
193
|
+
|
|
194
|
+
succeeding = Thread.new { instrument('/a', 2) }
|
|
195
|
+
succeeding_entered.pop # joined the reservation, the client has nothing yet
|
|
196
|
+
|
|
197
|
+
failing_resume << true
|
|
198
|
+
failing.join # the reservation is given back here
|
|
199
|
+
|
|
200
|
+
succeeding_resume << true
|
|
201
|
+
succeeding.join # from now on the client holds {path: '/a'}
|
|
202
|
+
|
|
203
|
+
expect { instrument('/b') }.to raise_error(Fluent::Plugin::Prometheus::LabelSetLimitError)
|
|
204
|
+
expect(client_counter.values.keys).to eq([{path: '/a'}])
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Summary#observe increments the count and the sum one after the other, so a
|
|
210
|
+
# value which is not a number leaves the count incremented and raises on the
|
|
211
|
+
# sum. Giving the slot back would not take that half instrumented label set
|
|
212
|
+
# away from the client, so the value is refused before it reaches either.
|
|
213
|
+
describe 'a summary of a value which is not a number' do
|
|
214
|
+
let(:element) do
|
|
215
|
+
Fluent::Config::Element.new(
|
|
216
|
+
'metric', '',
|
|
217
|
+
{
|
|
218
|
+
'name' => 'limited',
|
|
219
|
+
'type' => 'summary',
|
|
220
|
+
'desc' => 'Something foo.',
|
|
221
|
+
'key' => 'foo',
|
|
222
|
+
'max_series_per_metric' => max_series_per_metric.to_s,
|
|
223
|
+
},
|
|
224
|
+
[Fluent::Config::Element.new('labels', '', {'path' => '$.path'}, [])]
|
|
225
|
+
)
|
|
226
|
+
end
|
|
227
|
+
let(:metric) { Fluent::Plugin::Prometheus::Summary.new(element, registry, {}, {}) }
|
|
228
|
+
let(:client_summary) do
|
|
229
|
+
metric
|
|
230
|
+
registry.get(:limited)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it 'leaves the client holding nothing' do
|
|
234
|
+
expect { instrument('/a', 'not a number') }.to raise_error(ArgumentError)
|
|
235
|
+
|
|
236
|
+
expect(client_summary.values).to be_empty
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
it 'does not take a slot' do
|
|
240
|
+
expect { instrument('/a', 'not a number') }.to raise_error(ArgumentError)
|
|
241
|
+
|
|
242
|
+
expect { instrument('/b', 2) }.not_to raise_error
|
|
243
|
+
expect(client_summary.values.keys).to eq([{path: '/b'}])
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
describe 'a metric name shared by two <metric> sections' do
|
|
248
|
+
# both sections instrument the same client metric, so counting per section
|
|
249
|
+
# would let it hold max_series_per_metric label sets twice over
|
|
250
|
+
let(:max_series_per_metric) { 2 }
|
|
251
|
+
let(:other_metric) { Fluent::Plugin::Prometheus::Counter.new(element, registry, {}, {}) }
|
|
252
|
+
|
|
253
|
+
def instrument_other(path, value = 1)
|
|
254
|
+
other_metric.instrument({'foo' => value, 'path' => path}, expander)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it 'wraps one and the same client metric' do
|
|
258
|
+
expect(other_metric.instance_variable_get(:@counter))
|
|
259
|
+
.to equal(metric.instance_variable_get(:@counter))
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it 'counts the label sets of both sections against one limit' do
|
|
263
|
+
instrument('/a')
|
|
264
|
+
instrument_other('/b')
|
|
265
|
+
|
|
266
|
+
# the metric is full, whichever section the next record goes through
|
|
267
|
+
expect { instrument_other('/c') }.to raise_error(Fluent::Plugin::Prometheus::LabelSetLimitError)
|
|
268
|
+
expect { instrument('/d') }.to raise_error(Fluent::Plugin::Prometheus::LabelSetLimitError)
|
|
269
|
+
expect(client_counter.values.keys).to contain_exactly({path: '/a'}, {path: '/b'})
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it 'lets both sections instrument a label set the metric already holds' do
|
|
273
|
+
instrument('/a')
|
|
274
|
+
|
|
275
|
+
expect { instrument_other('/a', 2) }.not_to raise_error
|
|
276
|
+
expect(client_counter.values[{path: '/a'}]).to eq(3)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
describe 'initialized true with <initlabels>' do
|
|
281
|
+
let(:initlabels) { ['/a', '/b', '/c'] }
|
|
282
|
+
let(:element) do
|
|
283
|
+
Fluent::Config::Element.new(
|
|
284
|
+
'metric', '',
|
|
285
|
+
{
|
|
286
|
+
'name' => 'limited',
|
|
287
|
+
'type' => 'counter',
|
|
288
|
+
'desc' => 'Something foo.',
|
|
289
|
+
'key' => 'foo',
|
|
290
|
+
'initialized' => 'true',
|
|
291
|
+
'max_series_per_metric' => max_series_per_metric.to_s,
|
|
292
|
+
},
|
|
293
|
+
[Fluent::Config::Element.new('labels', '', {'path' => '$.path'}, [])] +
|
|
294
|
+
initlabels.map { |path| Fluent::Config::Element.new('initlabels', '', {'path' => path}, []) }
|
|
295
|
+
)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
context 'with a limit below the number of <initlabels> label sets' do
|
|
299
|
+
# 3 label sets exist at startup, so a limit of 1 would drop every record
|
|
300
|
+
let(:max_series_per_metric) { 1 }
|
|
301
|
+
|
|
302
|
+
it 'stops at startup instead of dropping every record' do
|
|
303
|
+
expect { build_metrics(element) }
|
|
304
|
+
.to raise_error(Fluent::ConfigError,
|
|
305
|
+
/already holds 3 label sets from <initlabels>.*max_series_per_metric is 1/)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
context 'with a limit equal to the number of <initlabels> label sets' do
|
|
310
|
+
# every label set is known in advance, so the limit is reached but no
|
|
311
|
+
# record is dropped
|
|
312
|
+
let(:max_series_per_metric) { 3 }
|
|
313
|
+
|
|
314
|
+
it 'accepts the config' do
|
|
315
|
+
expect { build_metrics(element) }.not_to raise_error
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
it 'still counts a record on an <initlabels> label set' do
|
|
319
|
+
instrument('/a')
|
|
320
|
+
|
|
321
|
+
expect(client_counter.values[{path: '/a'}]).to eq(1)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it 'refuses a label set which is not in <initlabels>' do
|
|
325
|
+
expect { instrument('/d') }.to raise_error(Fluent::Plugin::Prometheus::LabelSetLimitError)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
context 'with two <initlabels> holding the same values' do
|
|
330
|
+
# both make the same label set, so they take one slot
|
|
331
|
+
let(:initlabels) { ['/a', '/a'] }
|
|
332
|
+
let(:max_series_per_metric) { 1 }
|
|
333
|
+
|
|
334
|
+
it 'counts the label sets and not the <initlabels> blocks' do
|
|
335
|
+
expect { build_metrics(element) }.not_to raise_error
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
context 'without a limit' do
|
|
340
|
+
let(:max_series_per_metric) { 0 }
|
|
341
|
+
|
|
342
|
+
it 'accepts any number of <initlabels> label sets' do
|
|
343
|
+
expect { build_metrics(element) }.not_to raise_error
|
|
344
|
+
expect { instrument('/d') }.not_to raise_error
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
describe '<initlabels> shared by <metric> sections with the same name' do
|
|
350
|
+
# Every section with this name instruments the same client metric. The
|
|
351
|
+
# label sets from <initlabels> count in every section, even in one that
|
|
352
|
+
# declares none. A section with no limit still adds its own to the count.
|
|
353
|
+
let(:five_initlabels) { ['/a', '/b', '/c', '/d', '/e'] }
|
|
354
|
+
|
|
355
|
+
it 'refuses a section that has no <initlabels>' do
|
|
356
|
+
expect { build_metrics(counter_element(10, five_initlabels), counter_element(3)) }
|
|
357
|
+
.to raise_error(Fluent::ConfigError,
|
|
358
|
+
/already holds 5 label sets from <initlabels>.*max_series_per_metric is 3/)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
it 'counts the <initlabels> of a section that has no limit' do
|
|
362
|
+
expect { build_metrics(counter_element(0, five_initlabels), counter_element(3, ['/z'])) }
|
|
363
|
+
.to raise_error(Fluent::ConfigError,
|
|
364
|
+
/already holds 6 label sets from <initlabels>.*max_series_per_metric is 3/)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
it 'does not depend on the order of the sections' do
|
|
368
|
+
expect { build_metrics(counter_element(3), counter_element(10, five_initlabels)) }
|
|
369
|
+
.to raise_error(Fluent::ConfigError, /max_series_per_metric is 3/)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
it 'accepts a config when every limit fits the shared label sets' do
|
|
373
|
+
expect { build_metrics(counter_element(5, five_initlabels), counter_element(10)) }
|
|
374
|
+
.not_to raise_error
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
it 'counts them against the limit at runtime as well' do
|
|
378
|
+
# the section with no limit puts them on the client, so the section with
|
|
379
|
+
# a limit has to see them: none of its six slots is left
|
|
380
|
+
_unlimited, limited = build_metrics(counter_element(0, five_initlabels),
|
|
381
|
+
counter_element(6, ['/z']))
|
|
382
|
+
|
|
383
|
+
expect { limited.instrument({'foo' => 1, 'path' => '/new'}, expander) }
|
|
384
|
+
.to raise_error(Fluent::Plugin::Prometheus::LabelSetLimitError)
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
describe 'reading the configuration again' do
|
|
389
|
+
# A reload builds the <metric> sections again against the registry the
|
|
390
|
+
# process already has, so the client metric keeps its label sets. Reading
|
|
391
|
+
# the configuration twice here does the same.
|
|
392
|
+
def instrument_through(metric, path)
|
|
393
|
+
metric.instrument({'foo' => 1, 'path' => path}, expander)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
it 'does not count the label sets that records brought' do
|
|
397
|
+
# the section with the wider limit fills five slots of the set both
|
|
398
|
+
# sections share, which leaves none for the limit of three
|
|
399
|
+
wide, _narrow = build_metrics(counter_element(10), counter_element(3))
|
|
400
|
+
['/a', '/b', '/c', '/d', '/e'].each { |path| instrument_through(wide, path) }
|
|
401
|
+
|
|
402
|
+
expect { build_metrics(counter_element(10), counter_element(3)) }.not_to raise_error
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
it 'accepts the same <initlabels> again' do
|
|
406
|
+
metric, = build_metrics(counter_element(3, ['/a', '/b', '/c']))
|
|
407
|
+
instrument_through(metric, '/a')
|
|
408
|
+
|
|
409
|
+
expect { build_metrics(counter_element(3, ['/a', '/b', '/c'])) }.not_to raise_error
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
it 'keeps counting an <initlabels> label set a record came on' do
|
|
413
|
+
# the client still holds the three label sets, so a limit of 2 does not
|
|
414
|
+
# fit them even though a record made one of them look like its own
|
|
415
|
+
metric, = build_metrics(counter_element(3, ['/a', '/b', '/c']))
|
|
416
|
+
instrument_through(metric, '/a')
|
|
417
|
+
|
|
418
|
+
expect { build_metrics(counter_element(2, ['/a', '/b', '/c'])) }
|
|
419
|
+
.to raise_error(Fluent::ConfigError,
|
|
420
|
+
/already holds 3 label sets from <initlabels>.*max_series_per_metric is 2/)
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
describe '<metric> overriding the plugin limit' do
|
|
425
|
+
# the plugin is configured with 100, which <metric> has to win over
|
|
426
|
+
let(:metric) do
|
|
427
|
+
Fluent::Plugin::Prometheus::Counter.new(element, registry, {}, {max_series_per_metric: 100})
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
it 'narrows down the limit given to the plugin' do
|
|
431
|
+
expect(metric.max_series_per_metric).to eq(1)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
context 'with a limit above the one given to the plugin' do
|
|
435
|
+
let(:max_series_per_metric) { 1000 }
|
|
436
|
+
|
|
437
|
+
it 'widens the limit given to the plugin' do
|
|
438
|
+
expect(metric.max_series_per_metric).to eq(1000)
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
context 'with 0' do
|
|
443
|
+
let(:max_series_per_metric) { 0 }
|
|
444
|
+
|
|
445
|
+
it 'lifts the limit given to the plugin' do
|
|
446
|
+
expect(metric.max_series_per_metric).to eq(0)
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
context 'without a limit in <metric>' do
|
|
451
|
+
let(:element) do
|
|
452
|
+
Fluent::Config::Element.new(
|
|
453
|
+
'metric', '',
|
|
454
|
+
{
|
|
455
|
+
'name' => 'limited',
|
|
456
|
+
'type' => 'counter',
|
|
457
|
+
'desc' => 'Something foo.',
|
|
458
|
+
'key' => 'foo',
|
|
459
|
+
},
|
|
460
|
+
[Fluent::Config::Element.new('labels', '', {'path' => '$.path'}, [])]
|
|
461
|
+
)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
it 'falls back to the limit given to the plugin' do
|
|
465
|
+
expect(metric.max_series_per_metric).to eq(100)
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
end
|