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
|
@@ -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
|
|
@@ -331,4 +354,184 @@ describe Fluent::Plugin::PrometheusInput do
|
|
|
331
354
|
include_examples 'IPv6 server binding', '[::1]', '::1', 'handles pre-bracketed address correctly'
|
|
332
355
|
end
|
|
333
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
|
|
334
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
|
|
@@ -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
|
|