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
|
@@ -45,4 +45,145 @@ describe Fluent::Plugin::PrometheusFilter do
|
|
|
45
45
|
|
|
46
46
|
it_behaves_like 'instruments record'
|
|
47
47
|
end
|
|
48
|
+
|
|
49
|
+
# a label which uses the tag must be expanded with the tag, not with the
|
|
50
|
+
# record.
|
|
51
|
+
describe 'a record which has a tag placeholder name' do
|
|
52
|
+
let(:config) {
|
|
53
|
+
BASE_CONFIG + %(
|
|
54
|
+
<metric>
|
|
55
|
+
name tagged
|
|
56
|
+
type counter
|
|
57
|
+
desc Something foo.
|
|
58
|
+
key foo
|
|
59
|
+
<labels>
|
|
60
|
+
part ${tag_parts[0]}
|
|
61
|
+
</labels>
|
|
62
|
+
</metric>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
it 'labels the metric with the tag' do
|
|
67
|
+
driver.run(default_tag: tag) do
|
|
68
|
+
driver.feed(event_time, {'tag' => 'forged.tag', 'foo' => 1, 'tag_parts' => ['forged']})
|
|
69
|
+
end
|
|
70
|
+
# even though tag_parts exists in the record, it should not be taken as "part".
|
|
71
|
+
expect(registry.get(:tagged).values.keys).to eq([{part: 'prometheus'}])
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'labels the metric with the tag, not with the record key' do
|
|
75
|
+
driver.run(default_tag: tag) do
|
|
76
|
+
driver.feed(event_time, {'tag' => 'forged.tag', 'foo' => 1, 'tag_parts[0]' => 'forged'})
|
|
77
|
+
end
|
|
78
|
+
# the record has a key named "tag_parts[0]", but the tag must win.
|
|
79
|
+
expect(registry.get(:tagged).values.keys).to eq([{part: 'prometheus'}])
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# a record must not fill an index which the tag does not have.
|
|
84
|
+
describe 'a record which uses an index out of the tag' do
|
|
85
|
+
let(:config) {
|
|
86
|
+
BASE_CONFIG + %(
|
|
87
|
+
<metric>
|
|
88
|
+
name out_of_range
|
|
89
|
+
type counter
|
|
90
|
+
desc Something foo.
|
|
91
|
+
key foo
|
|
92
|
+
<labels>
|
|
93
|
+
part ${tag_parts[2]}
|
|
94
|
+
prefix ${tag_prefix[-1]}
|
|
95
|
+
suffix ${tag_suffix[-1]}
|
|
96
|
+
</labels>
|
|
97
|
+
</metric>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
it 'leaves the label unexpanded' do
|
|
102
|
+
driver.run(default_tag: tag) do
|
|
103
|
+
driver.feed(event_time, {
|
|
104
|
+
'foo' => 1,
|
|
105
|
+
'tag_parts[2]' => 'forged',
|
|
106
|
+
'tag_prefix[-1]' => 'forged',
|
|
107
|
+
'tag_suffix[-1]' => 'forged',
|
|
108
|
+
})
|
|
109
|
+
end
|
|
110
|
+
# the tag "prometheus.test" has 2 parts, and tag_prefix and tag_suffix
|
|
111
|
+
# have no negative index. So these labels must not be expanded.
|
|
112
|
+
expect(registry.get(:out_of_range).values.keys).to eq(
|
|
113
|
+
[{part: '${tag_parts[2]}', prefix: '${tag_prefix[-1]}', suffix: '${tag_suffix[-1]}'}]
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe 'limiting label expansion' do
|
|
119
|
+
it_behaves_like 'limits label expansion'
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# the throttling itself is covered by the LogThrottle spec; what is left here
|
|
123
|
+
# is the warning warn_label_set_limit builds out of it, and the interval it
|
|
124
|
+
# takes from the configuration
|
|
125
|
+
describe 'drop log throttling' do
|
|
126
|
+
let(:config) {
|
|
127
|
+
BASE_CONFIG + %[
|
|
128
|
+
ignore_error_log_interval 3600
|
|
129
|
+
<metric>
|
|
130
|
+
name throttled
|
|
131
|
+
type counter
|
|
132
|
+
desc Something foo.
|
|
133
|
+
key foo
|
|
134
|
+
</metric>
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
# Fluent::Clock.now is monotonic, so a plain Hash is enough to drive it
|
|
138
|
+
let(:clock) { { now: 1000.0 } }
|
|
139
|
+
let(:metric) { double('metric', name: :throttled, max_series_per_metric: 5) }
|
|
140
|
+
let(:text) { 'dropped a label set' }
|
|
141
|
+
|
|
142
|
+
before do
|
|
143
|
+
allow(Fluent::Clock).to receive(:now) { clock[:now] }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def logs_about(text)
|
|
147
|
+
driver.logs.select { |log| log.include?(text) }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def drop
|
|
151
|
+
driver.instance.send(:warn_label_set_limit, metric)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it 'warns only once within ignore_error_log_interval' do
|
|
155
|
+
5.times { drop }
|
|
156
|
+
expect(logs_about(text).size).to eq(1)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'reports how many warnings were suppressed in the meantime' do
|
|
160
|
+
3.times { drop }
|
|
161
|
+
clock[:now] += driver.instance.ignore_error_log_interval
|
|
162
|
+
drop
|
|
163
|
+
logs = logs_about(text)
|
|
164
|
+
expect(logs.size).to eq(2)
|
|
165
|
+
expect(logs.first).not_to include('suppressed_log_count')
|
|
166
|
+
expect(logs.last).to include('suppressed_log_count=2')
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# the only example which takes the interval from the configuration
|
|
170
|
+
context 'with ignore_error_log_interval 0' do
|
|
171
|
+
let(:config) {
|
|
172
|
+
BASE_CONFIG + %[
|
|
173
|
+
ignore_error_log_interval 0
|
|
174
|
+
<metric>
|
|
175
|
+
name throttled
|
|
176
|
+
type counter
|
|
177
|
+
desc Something foo.
|
|
178
|
+
key foo
|
|
179
|
+
</metric>
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
it 'warns about every drop' do
|
|
184
|
+
3.times { drop }
|
|
185
|
+
expect(logs_about(text).size).to eq(3)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
48
189
|
end
|
|
@@ -22,9 +22,16 @@ describe Fluent::Plugin::PrometheusInput do
|
|
|
22
22
|
describe '#configure' do
|
|
23
23
|
describe 'bind' do
|
|
24
24
|
let(:config) { CONFIG + %[
|
|
25
|
-
bind
|
|
25
|
+
bind ::1
|
|
26
26
|
] }
|
|
27
27
|
it 'should be configurable' do
|
|
28
|
+
expect(driver.instance.bind).to eq('::1')
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe 'default bind' do
|
|
33
|
+
let(:config) { CONFIG }
|
|
34
|
+
it 'should be accessible from 127.0.0.1 by default' do
|
|
28
35
|
expect(driver.instance.bind).to eq('127.0.0.1')
|
|
29
36
|
end
|
|
30
37
|
end
|
|
@@ -64,6 +71,22 @@ describe Fluent::Plugin::PrometheusInput do
|
|
|
64
71
|
expect(driver.instance.content_encoding).to eq(:gzip)
|
|
65
72
|
end
|
|
66
73
|
end
|
|
74
|
+
|
|
75
|
+
describe 'default ignore_error_log_interval' do
|
|
76
|
+
let(:config) { CONFIG }
|
|
77
|
+
it 'should be 3600 seconds by default' do
|
|
78
|
+
expect(driver.instance.ignore_error_log_interval).to eq(3600)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe 'error_log_interval' do
|
|
83
|
+
let(:config) { CONFIG + %[
|
|
84
|
+
ignore_error_log_interval 60
|
|
85
|
+
] }
|
|
86
|
+
it 'should be configurable' do
|
|
87
|
+
expect(driver.instance.ignore_error_log_interval).to eq(60)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
67
90
|
end
|
|
68
91
|
|
|
69
92
|
describe '#start' do
|
|
@@ -89,6 +112,22 @@ describe Fluent::Plugin::PrometheusInput do
|
|
|
89
112
|
end
|
|
90
113
|
end
|
|
91
114
|
|
|
115
|
+
context 'IPv6 with TLS' do
|
|
116
|
+
let(:config) do
|
|
117
|
+
%[
|
|
118
|
+
@type prometheus
|
|
119
|
+
bind ::1
|
|
120
|
+
<transport tls>
|
|
121
|
+
insecure true
|
|
122
|
+
</transport>
|
|
123
|
+
]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'raises ConfigError for unsupported combination' do
|
|
127
|
+
expect { driver.run(timeout: 1) }.to raise_error(Fluent::ConfigError, /IPv6 with <transport tls> is not currently supported/)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
92
131
|
context 'old parameters are given' do
|
|
93
132
|
context 'when extra_conf is used' do
|
|
94
133
|
let(:config) do
|
|
@@ -278,4 +317,221 @@ describe Fluent::Plugin::PrometheusInput do
|
|
|
278
317
|
end
|
|
279
318
|
end
|
|
280
319
|
end
|
|
320
|
+
|
|
321
|
+
describe '#run with IPv6' do
|
|
322
|
+
shared_examples 'IPv6 server binding' do |bind_addr, connect_addr, description|
|
|
323
|
+
let(:config) do
|
|
324
|
+
# Quote the bind address if it contains brackets
|
|
325
|
+
bind_value = bind_addr.include?('[') ? "\"#{bind_addr}\"" : bind_addr
|
|
326
|
+
%[
|
|
327
|
+
@type prometheus
|
|
328
|
+
bind #{bind_value}
|
|
329
|
+
]
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
it description do
|
|
333
|
+
skip 'IPv6 not available on this system' unless ipv6_enabled?
|
|
334
|
+
|
|
335
|
+
driver.run(timeout: 3) do
|
|
336
|
+
Net::HTTP.start(connect_addr, port) do |http|
|
|
337
|
+
req = Net::HTTP::Get.new('/metrics')
|
|
338
|
+
res = http.request(req)
|
|
339
|
+
expect(res.code).to eq('200')
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
context 'IPv6 loopback address ::1' do
|
|
346
|
+
include_examples 'IPv6 server binding', '::1', '::1', 'binds and serves on IPv6 loopback address'
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
context 'IPv6 any address ::' do
|
|
350
|
+
include_examples 'IPv6 server binding', '::', '::1', 'binds on :: and connects via ::1'
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
context 'pre-bracketed IPv6 address [::1]' do
|
|
354
|
+
include_examples 'IPv6 server binding', '[::1]', '::1', 'handles pre-bracketed address correctly'
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
describe 'error handling (information disclosure)' do
|
|
359
|
+
let(:config) { LOCAL_CONFIG }
|
|
360
|
+
let(:secret_message) { 'dummy secret detail: password=deadbeef' }
|
|
361
|
+
|
|
362
|
+
shared_examples 'suppressed exception response' do
|
|
363
|
+
it 'returns 500 with text/plain' do
|
|
364
|
+
status, header, _body = subject
|
|
365
|
+
expect(status).to eq(500)
|
|
366
|
+
expect(header['Content-Type']).to eq('text/plain')
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
it 'exposes the exception class only' do
|
|
370
|
+
_status, _header, body = subject
|
|
371
|
+
expect(body).to eq('in_prometheus server error: <RuntimeError>')
|
|
372
|
+
expect(body).not_to include(secret_message)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
it 'logs the detail on the server side' do
|
|
376
|
+
subject
|
|
377
|
+
expect(driver.logs.any? { |log| log.include?(log_message) }).to be true
|
|
378
|
+
expect(driver.logs.any? { |log| log.include?(secret_message) }).to be true
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
context '#all_metrics' do
|
|
383
|
+
subject { driver.instance.send(:all_metrics) }
|
|
384
|
+
|
|
385
|
+
let(:log_message) { 'in_prometheus: failed to render metrics' }
|
|
386
|
+
|
|
387
|
+
before do
|
|
388
|
+
allow(::Prometheus::Client::Formats::Text).to receive(:marshal).and_raise(RuntimeError, secret_message)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
include_examples 'suppressed exception response'
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
context '#all_workers_metrics' do
|
|
395
|
+
subject { driver.instance.send(:all_workers_metrics) }
|
|
396
|
+
|
|
397
|
+
let(:log_message) { 'in_prometheus: failed to render workers metrics' }
|
|
398
|
+
|
|
399
|
+
before do
|
|
400
|
+
allow(driver.instance).to receive(:send_request_to_each_worker).and_raise(RuntimeError, secret_message)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
include_examples 'suppressed exception response'
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
context 'over HTTP' do
|
|
407
|
+
before do
|
|
408
|
+
allow(::Prometheus::Client::Formats::Text).to receive(:marshal).and_raise(RuntimeError, secret_message)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
it 'does not leak the exception message to the client' do
|
|
412
|
+
driver.run(timeout: 1) do
|
|
413
|
+
Net::HTTP.start('127.0.0.1', port) do |http|
|
|
414
|
+
req = Net::HTTP::Get.new('/metrics')
|
|
415
|
+
res = http.request(req)
|
|
416
|
+
expect(res.code).to eq('500')
|
|
417
|
+
expect(res.body).to eq('in_prometheus server error: <RuntimeError>')
|
|
418
|
+
expect(res.body).not_to include(secret_message)
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# The LogThrottle spec covers the throttling itself. These examples cover how
|
|
426
|
+
# in_prometheus uses it: the scope it throttles on, the fingerprint it makes
|
|
427
|
+
# from an error, the suppressed_log_count in the log, and what the client
|
|
428
|
+
# gets meanwhile.
|
|
429
|
+
describe 'error log throttling' do
|
|
430
|
+
let(:config) { LOCAL_CONFIG }
|
|
431
|
+
let(:secret_message) { 'dummy secret detail: password=deadbeef' }
|
|
432
|
+
let(:log_message) { 'in_prometheus: failed to render metrics' }
|
|
433
|
+
let(:workers_log_message) { 'in_prometheus: failed to render workers metrics' }
|
|
434
|
+
|
|
435
|
+
# Fluent::Clock.now is monotonic, so a plain Hash is enough to drive it
|
|
436
|
+
let(:clock) { { now: 1000.0 } }
|
|
437
|
+
|
|
438
|
+
def error_logs(message)
|
|
439
|
+
driver.logs.select { |log| log.include?(message) }
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
context 'when rendering metrics keeps failing' do
|
|
443
|
+
before do
|
|
444
|
+
allow(Fluent::Clock).to receive(:now) { clock[:now] }
|
|
445
|
+
allow(::Prometheus::Client::Formats::Text).to receive(:marshal).and_raise(RuntimeError, secret_message)
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# every iteration raises from the same line, so the exceptions are equal
|
|
449
|
+
# to each other and only ignore_error_log_interval can let a log through
|
|
450
|
+
it 'logs the repeated same failure only once within ignore_error_log_interval' do
|
|
451
|
+
5.times { driver.instance.send(:all_metrics) }
|
|
452
|
+
expect(error_logs(log_message).size).to eq(1)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
it 'keeps returning 500 to the client even while the log is suppressed' do
|
|
456
|
+
responses = 5.times.map { driver.instance.send(:all_metrics) }
|
|
457
|
+
expect(error_logs(log_message).size).to eq(1)
|
|
458
|
+
responses.each do |status, _header, body|
|
|
459
|
+
expect(status).to eq(500)
|
|
460
|
+
expect(body).to eq('in_prometheus server error: <RuntimeError>')
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
it 'reports how many logs were suppressed in the meantime' do
|
|
465
|
+
3.times { driver.instance.send(:all_metrics) }
|
|
466
|
+
clock[:now] += driver.instance.ignore_error_log_interval
|
|
467
|
+
driver.instance.send(:all_metrics)
|
|
468
|
+
logs = error_logs(log_message)
|
|
469
|
+
expect(logs.size).to eq(2)
|
|
470
|
+
expect(logs.first).not_to include('suppressed_log_count')
|
|
471
|
+
expect(logs.last).to include('suppressed_log_count=2')
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
describe 'telling the errors apart' do
|
|
476
|
+
before do
|
|
477
|
+
allow(Fluent::Clock).to receive(:now) { clock[:now] }
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def log_error(scope, message, error)
|
|
481
|
+
driver.instance.send(:log_error_throttled, scope, message, error: error)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# the class and the message both take part in the fingerprint
|
|
485
|
+
it 'logs immediately when the error class differs' do
|
|
486
|
+
log_error(:metrics, log_message, RuntimeError.new(secret_message))
|
|
487
|
+
log_error(:metrics, log_message, ArgumentError.new(secret_message))
|
|
488
|
+
expect(error_logs(log_message).size).to eq(2)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
it 'logs immediately when the error differs' do
|
|
492
|
+
log_error(:metrics, log_message, RuntimeError.new(secret_message))
|
|
493
|
+
log_error(:metrics, log_message, RuntimeError.new('another failure'))
|
|
494
|
+
expect(error_logs(log_message).size).to eq(2)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# the scope, not the log message, picks the slot to throttle on
|
|
498
|
+
it 'suppresses an equal error within a scope even when the log message differs' do
|
|
499
|
+
error = RuntimeError.new(secret_message)
|
|
500
|
+
log_error(:metrics, log_message, error)
|
|
501
|
+
log_error(:metrics, workers_log_message, error)
|
|
502
|
+
expect(error_logs(workers_log_message)).to be_empty
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# the only example which takes the interval from the configuration
|
|
506
|
+
context 'with ignore_error_log_interval 0' do
|
|
507
|
+
let(:config) { LOCAL_CONFIG + %[
|
|
508
|
+
ignore_error_log_interval 0
|
|
509
|
+
] }
|
|
510
|
+
|
|
511
|
+
it 'logs every occurrence of the same error' do
|
|
512
|
+
3.times { log_error(:metrics, log_message, RuntimeError.new(secret_message)) }
|
|
513
|
+
expect(error_logs(log_message).size).to eq(3)
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# /metrics and /aggregated_metrics are usually scraped in turn, so both of
|
|
519
|
+
# them must be throttled on their own slot
|
|
520
|
+
context 'when both endpoints keep failing alternately' do
|
|
521
|
+
before do
|
|
522
|
+
allow(Fluent::Clock).to receive(:now) { clock[:now] }
|
|
523
|
+
allow(::Prometheus::Client::Formats::Text).to receive(:marshal).and_raise(RuntimeError, secret_message)
|
|
524
|
+
allow(driver.instance).to receive(:send_request_to_each_worker).and_raise(ArgumentError, 'another failure')
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
it 'logs each failure only once within ignore_error_log_interval' do
|
|
528
|
+
5.times do
|
|
529
|
+
driver.instance.send(:all_metrics)
|
|
530
|
+
driver.instance.send(:all_workers_metrics)
|
|
531
|
+
end
|
|
532
|
+
expect(error_logs(log_message).size).to eq(1)
|
|
533
|
+
expect(error_logs(workers_log_message).size).to eq(1)
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
end
|
|
281
537
|
end
|
|
@@ -19,7 +19,35 @@ describe Fluent::Plugin::PrometheusOutput do
|
|
|
19
19
|
describe '#testinitlabels' do
|
|
20
20
|
it_behaves_like 'initalized metrics'
|
|
21
21
|
end
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
describe 'limiting label expansion' do
|
|
24
|
+
it_behaves_like 'limits label expansion'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# filter_prometheus routes such a record to @ERROR already. The output has to
|
|
28
|
+
# do the same, instead of failing on the router itself.
|
|
29
|
+
describe 'a record which cannot be instrumented' do
|
|
30
|
+
let(:config) {
|
|
31
|
+
BASE_CONFIG + %(
|
|
32
|
+
<metric>
|
|
33
|
+
name failing
|
|
34
|
+
type counter
|
|
35
|
+
desc Something foo.
|
|
36
|
+
key foo
|
|
37
|
+
</metric>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
it 'emits an error event' do
|
|
42
|
+
driver.run(default_tag: tag) do
|
|
43
|
+
# a non numeric value is refused when the metric is instrumented
|
|
44
|
+
driver.feed(event_time, {'foo' => 'not a number'})
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
expect(driver.error_events.size).to eq(1)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
23
51
|
describe '#run' do
|
|
24
52
|
let(:message) { {"foo" => 100, "bar" => 100, "baz" => 100, "qux" => 10} }
|
|
25
53
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'fluent/plugin/prometheus/log_throttle'
|
|
3
|
+
|
|
4
|
+
describe Fluent::Plugin::Prometheus::LogThrottle do
|
|
5
|
+
# Fluent::Clock.now is monotonic, so a Hash is enough to fake it
|
|
6
|
+
let(:clock) { { now: 1000.0 } }
|
|
7
|
+
let(:interval) { 3600 }
|
|
8
|
+
# in_prometheus builds it out of an error
|
|
9
|
+
let(:fingerprint) { [RuntimeError, 'a'] }
|
|
10
|
+
subject(:throttle) { described_class.new(interval) }
|
|
11
|
+
|
|
12
|
+
before do
|
|
13
|
+
allow(Fluent::Clock).to receive(:now) { clock[:now] }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe '#check' do
|
|
17
|
+
it 'emits on the first occurrence of a key' do
|
|
18
|
+
emit, suppressed = throttle.check(:foo, fingerprint)
|
|
19
|
+
expect(emit).to be true
|
|
20
|
+
expect(suppressed).to eq(0)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'suppresses the same key within the interval' do
|
|
24
|
+
throttle.check(:foo, fingerprint)
|
|
25
|
+
clock[:now] += interval - 1
|
|
26
|
+
emit, _ = throttle.check(:foo, fingerprint)
|
|
27
|
+
expect(emit).to be false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'emits again once the interval has elapsed' do
|
|
31
|
+
throttle.check(:foo, fingerprint)
|
|
32
|
+
clock[:now] += interval
|
|
33
|
+
emit, _ = throttle.check(:foo, fingerprint)
|
|
34
|
+
expect(emit).to be true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'reports how many occurrences were suppressed in the meantime' do
|
|
38
|
+
throttle.check(:foo, fingerprint) # emits, suppressed=0
|
|
39
|
+
2.times { throttle.check(:foo, fingerprint) } # suppressed 1, then 2
|
|
40
|
+
clock[:now] += interval
|
|
41
|
+
emit, suppressed = throttle.check(:foo, fingerprint)
|
|
42
|
+
expect(emit).to be true
|
|
43
|
+
expect(suppressed).to eq(2)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'resets the suppressed count after emitting' do
|
|
47
|
+
throttle.check(:foo, fingerprint)
|
|
48
|
+
2.times { throttle.check(:foo, fingerprint) }
|
|
49
|
+
clock[:now] += interval
|
|
50
|
+
throttle.check(:foo, fingerprint) # emits with suppressed=2
|
|
51
|
+
clock[:now] += interval
|
|
52
|
+
_, suppressed = throttle.check(:foo, fingerprint)
|
|
53
|
+
expect(suppressed).to eq(0)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'keeps a separate slot per key' do
|
|
57
|
+
expect(throttle.check(:foo, fingerprint).first).to be true
|
|
58
|
+
expect(throttle.check(:bar, fingerprint).first).to be true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# filter/out_prometheus throttles on the metric alone, since every drop of
|
|
62
|
+
# a metric reads the same
|
|
63
|
+
context 'without a fingerprint' do
|
|
64
|
+
it 'throttles on the key alone' do
|
|
65
|
+
expect(throttle.check(:foo).first).to be true
|
|
66
|
+
expect(throttle.check(:foo).first).to be false
|
|
67
|
+
expect(throttle.check(:bar).first).to be true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'reports how many occurrences were suppressed in the meantime' do
|
|
71
|
+
throttle.check(:foo)
|
|
72
|
+
2.times { throttle.check(:foo) }
|
|
73
|
+
clock[:now] += interval
|
|
74
|
+
emit, suppressed = throttle.check(:foo)
|
|
75
|
+
expect(emit).to be true
|
|
76
|
+
expect(suppressed).to eq(2)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'emits immediately when the fingerprint changes within the interval' do
|
|
81
|
+
expect(throttle.check(:foo, [RuntimeError, 'a']).first).to be true
|
|
82
|
+
expect(throttle.check(:foo, [RuntimeError, 'b']).first).to be true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# the caller makes a new fingerprint for each event, so it has to be
|
|
86
|
+
# compared by value, not by object identity
|
|
87
|
+
it 'suppresses an equal fingerprint given as a different object' do
|
|
88
|
+
expect(throttle.check(:foo, [RuntimeError, 'a']).first).to be true
|
|
89
|
+
expect(throttle.check(:foo, [RuntimeError, 'a']).first).to be false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'does not carry the suppressed count across a fingerprint change' do
|
|
93
|
+
throttle.check(:foo, [RuntimeError, 'a'])
|
|
94
|
+
2.times { throttle.check(:foo, [RuntimeError, 'a']) }
|
|
95
|
+
emit, suppressed = throttle.check(:foo, [RuntimeError, 'b'])
|
|
96
|
+
expect(emit).to be true
|
|
97
|
+
expect(suppressed).to eq(0)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
context 'when interval is zero' do
|
|
101
|
+
let(:interval) { 0 }
|
|
102
|
+
|
|
103
|
+
it 'always emits without consulting the clock' do
|
|
104
|
+
expect(Fluent::Clock).not_to receive(:now)
|
|
105
|
+
3.times do
|
|
106
|
+
emit, suppressed = throttle.check(:foo, fingerprint)
|
|
107
|
+
expect(emit).to be true
|
|
108
|
+
expect(suppressed).to eq(0)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
context 'when interval is negative' do
|
|
114
|
+
let(:interval) { -1 }
|
|
115
|
+
|
|
116
|
+
it 'always emits' do
|
|
117
|
+
expect(throttle.check(:foo, fingerprint).first).to be true
|
|
118
|
+
expect(throttle.check(:foo, fingerprint).first).to be true
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'serializes concurrent checks for the same key into a single emission' do
|
|
123
|
+
results = 10.times.map { Thread.new { throttle.check(:foo, fingerprint).first } }.map(&:value)
|
|
124
|
+
expect(results.count(true)).to eq(1)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|