fluent-plugin-prometheus 2.2.2 → 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 +2 -2
- data/ChangeLog +13 -0
- data/README.md +128 -1
- data/fluent-plugin-prometheus.gemspec +1 -1
- data/lib/fluent/plugin/filter_prometheus.rb +1 -1
- data/lib/fluent/plugin/in_prometheus.rb +23 -3
- 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 +204 -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 +107 -0
- metadata +11 -2
|
@@ -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
|
|
@@ -390,6 +390,113 @@ shared_examples_for 'instruments record' do
|
|
|
390
390
|
end
|
|
391
391
|
end
|
|
392
392
|
|
|
393
|
+
shared_examples_for 'limits label expansion' do
|
|
394
|
+
# the limit is enforced by the shared Metric class, but each plugin reaches
|
|
395
|
+
# it through its own path (instrument_single vs instrument), so both are run
|
|
396
|
+
# against these examples
|
|
397
|
+
def limited_config(options)
|
|
398
|
+
BASE_CONFIG + options + %[
|
|
399
|
+
<metric>
|
|
400
|
+
name limited
|
|
401
|
+
type counter
|
|
402
|
+
desc Something foo.
|
|
403
|
+
key foo
|
|
404
|
+
<labels>
|
|
405
|
+
path $.path
|
|
406
|
+
</labels>
|
|
407
|
+
</metric>
|
|
408
|
+
]
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def drop_logs
|
|
412
|
+
driver.logs.select { |log| log.include?('dropped a label set') }
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def dropped_label_sets
|
|
416
|
+
registry.metrics.find { |metric| metric.name == :fluentd_prometheus_dropped_label_sets_total }
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
let(:counter) { registry.get(:limited) }
|
|
420
|
+
|
|
421
|
+
context 'without any limit configured' do
|
|
422
|
+
let(:config) { limited_config('') }
|
|
423
|
+
|
|
424
|
+
it 'is unlimited by default' do
|
|
425
|
+
expect(driver.instance.max_series_per_metric).to eq(0)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
it 'keeps every label set' do
|
|
429
|
+
driver.run(default_tag: tag) do
|
|
430
|
+
driver.feed(event_time, {'foo' => 1, 'path' => '/a'})
|
|
431
|
+
driver.feed(event_time, {'foo' => 1, 'path' => '/b'})
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
expect(counter.values.keys).to eq([{path: '/a'}, {path: '/b'}])
|
|
435
|
+
expect(drop_logs).to be_empty
|
|
436
|
+
# nothing was dropped, so the counter is not even registered
|
|
437
|
+
expect(dropped_label_sets).to be_nil
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
context 'with max_series_per_metric' do
|
|
442
|
+
let(:config) { limited_config(%[max_series_per_metric 1\n]) }
|
|
443
|
+
|
|
444
|
+
it 'drops a new label set once the limit is reached' do
|
|
445
|
+
driver.run(default_tag: tag) do
|
|
446
|
+
driver.feed(event_time, {'foo' => 1, 'path' => '/a'})
|
|
447
|
+
driver.feed(event_time, {'foo' => 1, 'path' => '/b'})
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
expect(counter.values.keys).to eq([{path: '/a'}])
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
it 'keeps instrumenting a known label set after the limit is reached' do
|
|
454
|
+
driver.run(default_tag: tag) do
|
|
455
|
+
driver.feed(event_time, {'foo' => 1, 'path' => '/a'})
|
|
456
|
+
driver.feed(event_time, {'foo' => 1, 'path' => '/b'})
|
|
457
|
+
driver.feed(event_time, {'foo' => 1, 'path' => '/a'})
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
expect(counter.get(labels: {path: '/a'})).to eq(2)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
it 'does not consume the limit by a label set which failed to be instrumented' do
|
|
464
|
+
driver.run(default_tag: tag) do
|
|
465
|
+
# a non numeric value is refused before the label set is reserved
|
|
466
|
+
driver.feed(event_time, {'foo' => 'not a number', 'path' => '/a'})
|
|
467
|
+
driver.feed(event_time, {'foo' => 1, 'path' => '/b'})
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
expect(driver.error_events.size).to eq(1)
|
|
471
|
+
expect(counter.values.keys).to eq([{path: '/b'}])
|
|
472
|
+
expect(drop_logs).to be_empty
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
it 'counts every dropped record, while the log is throttled' do
|
|
476
|
+
driver.run(default_tag: tag) do
|
|
477
|
+
driver.feed(event_time, {'foo' => 1, 'path' => '/a'})
|
|
478
|
+
driver.feed(event_time, {'foo' => 1, 'path' => '/b'})
|
|
479
|
+
driver.feed(event_time, {'foo' => 1, 'path' => '/c'})
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
expect(dropped_label_sets.values).to eq({{name: 'limited'} => 2.0})
|
|
483
|
+
expect(drop_logs.size).to eq(1)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# the same label set is refused again and again, and each record which
|
|
487
|
+
# brought it is lost
|
|
488
|
+
it 'counts a label set which is dropped more than once every time' do
|
|
489
|
+
driver.run(default_tag: tag) do
|
|
490
|
+
driver.feed(event_time, {'foo' => 1, 'path' => '/a'})
|
|
491
|
+
driver.feed(event_time, {'foo' => 1, 'path' => '/b'})
|
|
492
|
+
driver.feed(event_time, {'foo' => 1, 'path' => '/b'})
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
expect(dropped_label_sets.values).to eq({{name: 'limited'} => 2.0})
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
|
|
393
500
|
shared_examples_for 'initalized metrics' do
|
|
394
501
|
before do
|
|
395
502
|
driver.run(default_tag: tag)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fluent-plugin-prometheus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Masahiro Sano
|
|
@@ -106,7 +106,11 @@ executables: []
|
|
|
106
106
|
extensions: []
|
|
107
107
|
extra_rdoc_files: []
|
|
108
108
|
files:
|
|
109
|
+
- ".github/ISSUE_TEMPLATE/bug_report.yaml"
|
|
110
|
+
- ".github/ISSUE_TEMPLATE/config.yml"
|
|
111
|
+
- ".github/ISSUE_TEMPLATE/feature_request.yaml"
|
|
109
112
|
- ".github/dependabot.yml"
|
|
113
|
+
- ".github/workflows/add-to-project.yml"
|
|
110
114
|
- ".github/workflows/linux.yml"
|
|
111
115
|
- ".gitignore"
|
|
112
116
|
- ".rspec"
|
|
@@ -124,6 +128,7 @@ files:
|
|
|
124
128
|
- lib/fluent/plugin/in_prometheus_tail_monitor.rb
|
|
125
129
|
- lib/fluent/plugin/out_prometheus.rb
|
|
126
130
|
- lib/fluent/plugin/prometheus.rb
|
|
131
|
+
- lib/fluent/plugin/prometheus/log_throttle.rb
|
|
127
132
|
- lib/fluent/plugin/prometheus/placeholder_expander.rb
|
|
128
133
|
- lib/fluent/plugin/prometheus_metrics.rb
|
|
129
134
|
- misc/fluentd_sample.conf
|
|
@@ -135,7 +140,9 @@ files:
|
|
|
135
140
|
- spec/fluent/plugin/in_prometheus_spec.rb
|
|
136
141
|
- spec/fluent/plugin/in_prometheus_tail_monitor_spec.rb
|
|
137
142
|
- spec/fluent/plugin/out_prometheus_spec.rb
|
|
143
|
+
- spec/fluent/plugin/prometheus/log_throttle_spec.rb
|
|
138
144
|
- spec/fluent/plugin/prometheus/placeholder_expander_spec.rb
|
|
145
|
+
- spec/fluent/plugin/prometheus/series_limit_spec.rb
|
|
139
146
|
- spec/fluent/plugin/prometheus_metrics_spec.rb
|
|
140
147
|
- spec/fluent/plugin/shared.rb
|
|
141
148
|
- spec/spec_helper.rb
|
|
@@ -157,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
157
164
|
- !ruby/object:Gem::Version
|
|
158
165
|
version: '0'
|
|
159
166
|
requirements: []
|
|
160
|
-
rubygems_version:
|
|
167
|
+
rubygems_version: 3.6.9
|
|
161
168
|
specification_version: 4
|
|
162
169
|
summary: A fluent plugin that collects metrics and exposes for Prometheus.
|
|
163
170
|
test_files:
|
|
@@ -166,7 +173,9 @@ test_files:
|
|
|
166
173
|
- spec/fluent/plugin/in_prometheus_spec.rb
|
|
167
174
|
- spec/fluent/plugin/in_prometheus_tail_monitor_spec.rb
|
|
168
175
|
- spec/fluent/plugin/out_prometheus_spec.rb
|
|
176
|
+
- spec/fluent/plugin/prometheus/log_throttle_spec.rb
|
|
169
177
|
- spec/fluent/plugin/prometheus/placeholder_expander_spec.rb
|
|
178
|
+
- spec/fluent/plugin/prometheus/series_limit_spec.rb
|
|
170
179
|
- spec/fluent/plugin/prometheus_metrics_spec.rb
|
|
171
180
|
- spec/fluent/plugin/shared.rb
|
|
172
181
|
- spec/spec_helper.rb
|