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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'prometheus/client'
|
|
2
2
|
require 'prometheus/client/formats/text'
|
|
3
|
+
require 'fluent/plugin/prometheus/log_throttle'
|
|
3
4
|
require 'fluent/plugin/prometheus/placeholder_expander'
|
|
4
5
|
|
|
5
6
|
module Fluent
|
|
@@ -31,6 +32,101 @@ module Fluent
|
|
|
31
32
|
|
|
32
33
|
module Prometheus
|
|
33
34
|
class AlreadyRegisteredError < StandardError; end
|
|
35
|
+
class LabelSetLimitError < StandardError; end
|
|
36
|
+
|
|
37
|
+
# 0 or less means unlimited. The limit is unlimited by default, because
|
|
38
|
+
# enabling it changes the existing metrics silently: dropping a label set
|
|
39
|
+
# loses the record without any way to recover it. An operator who needs
|
|
40
|
+
# to bound the cardinality has to opt in explicitly.
|
|
41
|
+
DEFAULT_MAX_SERIES_PER_METRIC = 0
|
|
42
|
+
DEFAULT_IGNORE_ERROR_LOG_INTERVAL = 3600
|
|
43
|
+
|
|
44
|
+
# the drops are visible in Prometheus, not only in the Fluentd log
|
|
45
|
+
DROPPED_LABEL_SETS_METRIC_NAME = :fluentd_prometheus_dropped_label_sets_total
|
|
46
|
+
DROPPED_LABEL_SETS_METRIC_DESC = 'The total number of records dropped because the metric reached max_series_per_metric.'
|
|
47
|
+
|
|
48
|
+
def self.included(klass)
|
|
49
|
+
klass.class_eval do
|
|
50
|
+
desc 'The maximum number of label sets a metric can hold. Exceeding label sets are dropped. 0 (default) means unlimited.'
|
|
51
|
+
config_param :max_series_per_metric, :integer, default: DEFAULT_MAX_SERIES_PER_METRIC
|
|
52
|
+
desc 'The interval to suppress the repeated warning about the drops.'
|
|
53
|
+
config_param :ignore_error_log_interval, :time, default: DEFAULT_IGNORE_ERROR_LOG_INTERVAL
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The label sets a client metric holds. The client registry keys its
|
|
58
|
+
# metrics by name alone, so every <metric> section with the same name
|
|
59
|
+
# instruments the same client metric and shares this set, instead of
|
|
60
|
+
# holding max_series_per_metric label sets of its own.
|
|
61
|
+
class SeriesSet
|
|
62
|
+
# The set is kept on the client metric, so that it is found again by
|
|
63
|
+
# every section. The registry does not drop its metrics, so the set is
|
|
64
|
+
# still there after a reload.
|
|
65
|
+
IVAR = :@fluent_plugin_prometheus_series_set
|
|
66
|
+
|
|
67
|
+
# Sections are built at configuration time, which is single threaded,
|
|
68
|
+
# so this needs no lock.
|
|
69
|
+
def self.of(client_metric)
|
|
70
|
+
client_metric.instance_variable_get(IVAR) ||
|
|
71
|
+
client_metric.instance_variable_set(IVAR, new)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def initialize
|
|
75
|
+
@series = {}
|
|
76
|
+
@mutex = Mutex.new
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Only <initlabels> come from the configuration. Counting a label set
|
|
80
|
+
# a record brought would refuse a good configuration after a reload.
|
|
81
|
+
def initial_size
|
|
82
|
+
@mutex.synchronize { @series.count { |_, state| state == :initial } }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Checking the limit and taking the slot happen under the same lock, so
|
|
86
|
+
# that concurrent calls cannot both take the last one. The slot stays
|
|
87
|
+
# :reserved until the instrumentation confirms it, so that a failing
|
|
88
|
+
# call can tell an in-flight reservation from a series the client holds.
|
|
89
|
+
def reserve(label, limit, name)
|
|
90
|
+
@mutex.synchronize do
|
|
91
|
+
next false if @series.key?(label)
|
|
92
|
+
|
|
93
|
+
if @series.size >= limit
|
|
94
|
+
raise LabelSetLimitError, "#{name} reached max_series_per_metric (#{limit})"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
@series[label] = :reserved
|
|
98
|
+
next true
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Marks a label set as established, once the client actually holds it.
|
|
103
|
+
# The limit is not checked on purpose: the series exists on the client
|
|
104
|
+
# side already, so it has to be accounted for even when a concurrent
|
|
105
|
+
# failure gave the reservation back in the meantime.
|
|
106
|
+
def confirm(label)
|
|
107
|
+
@mutex.synchronize do
|
|
108
|
+
# #initial_size has to keep counting it, so a record on it does not
|
|
109
|
+
# change where it came from
|
|
110
|
+
next if @series[label] == :initial
|
|
111
|
+
|
|
112
|
+
@series[label] = :confirmed
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The limit is not checked here either: a section which cannot hold
|
|
117
|
+
# these label sets is refused when the configuration is read.
|
|
118
|
+
def confirm_initial(label)
|
|
119
|
+
@mutex.synchronize { @series[label] = :initial }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Gives a reserved slot back when the instrumentation failed, so that a
|
|
123
|
+
# label set the client does not hold does not consume the limit. One
|
|
124
|
+
# which a concurrent call confirmed in the meantime is kept: the client
|
|
125
|
+
# holds that series.
|
|
126
|
+
def release(label)
|
|
127
|
+
@mutex.synchronize { @series.delete(label) if @series[label] == :reserved }
|
|
128
|
+
end
|
|
129
|
+
end
|
|
34
130
|
|
|
35
131
|
def self.parse_labels_elements(conf)
|
|
36
132
|
labels = conf.elements.select { |e| e.name == 'labels' }
|
|
@@ -119,7 +215,7 @@ module Fluent
|
|
|
119
215
|
base_initlabels
|
|
120
216
|
end
|
|
121
217
|
|
|
122
|
-
def self.parse_metrics_elements(conf, registry, labels = {})
|
|
218
|
+
def self.parse_metrics_elements(conf, registry, labels = {}, opts = {})
|
|
123
219
|
metrics = []
|
|
124
220
|
conf.elements.select { |element|
|
|
125
221
|
element.name == 'metric'
|
|
@@ -130,17 +226,25 @@ module Fluent
|
|
|
130
226
|
end
|
|
131
227
|
case element['type']
|
|
132
228
|
when 'summary'
|
|
133
|
-
metrics << Fluent::Plugin::Prometheus::Summary.new(element, registry, labels)
|
|
229
|
+
metrics << Fluent::Plugin::Prometheus::Summary.new(element, registry, labels, opts)
|
|
134
230
|
when 'gauge'
|
|
135
|
-
metrics << Fluent::Plugin::Prometheus::Gauge.new(element, registry, labels)
|
|
231
|
+
metrics << Fluent::Plugin::Prometheus::Gauge.new(element, registry, labels, opts)
|
|
136
232
|
when 'counter'
|
|
137
|
-
metrics << Fluent::Plugin::Prometheus::Counter.new(element, registry, labels)
|
|
233
|
+
metrics << Fluent::Plugin::Prometheus::Counter.new(element, registry, labels, opts)
|
|
138
234
|
when 'histogram'
|
|
139
|
-
metrics << Fluent::Plugin::Prometheus::Histogram.new(element, registry, labels)
|
|
235
|
+
metrics << Fluent::Plugin::Prometheus::Histogram.new(element, registry, labels, opts)
|
|
140
236
|
else
|
|
141
237
|
raise ConfigError, "type option must be 'counter', 'gauge', 'summary' or 'histogram'"
|
|
142
238
|
end
|
|
143
239
|
}
|
|
240
|
+
|
|
241
|
+
# <metric> sections with the same name share one client
|
|
242
|
+
# metric. All of their <initlabels> label sets are known only
|
|
243
|
+
# after every section is built, so the check runs here and not
|
|
244
|
+
# in the constructor not to depend on the order of the
|
|
245
|
+
# sections.
|
|
246
|
+
metrics.each(&:check_series_limit!)
|
|
247
|
+
|
|
144
248
|
metrics
|
|
145
249
|
end
|
|
146
250
|
|
|
@@ -165,6 +269,49 @@ module Fluent
|
|
|
165
269
|
@placeholder_values = {}
|
|
166
270
|
@placeholder_expander_builder = Fluent::Plugin::Prometheus.placeholder_expander(log)
|
|
167
271
|
@hostname = Socket.gethostname
|
|
272
|
+
@label_set_limit_log_throttle = Fluent::Plugin::Prometheus::LogThrottle.new(@ignore_error_log_interval)
|
|
273
|
+
@dropped_label_sets_counter = nil
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def metric_options
|
|
277
|
+
{
|
|
278
|
+
max_series_per_metric: @max_series_per_metric,
|
|
279
|
+
}
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Registered on the first occurrence only, so that a plugin which never
|
|
283
|
+
# drops anything does not expose a counter which stays 0 forever. Its
|
|
284
|
+
# labels come from the configuration, so they cannot blow up on their own.
|
|
285
|
+
def limit_counter(name, docstring, labels)
|
|
286
|
+
@registry.counter(name, docstring: docstring, labels: labels)
|
|
287
|
+
rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
|
|
288
|
+
# another plugin instance shares the registry and registered it first
|
|
289
|
+
Fluent::Plugin::Prometheus::Metric.get(@registry, name, :counter, docstring)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def dropped_label_sets_counter
|
|
293
|
+
@dropped_label_sets_counter ||=
|
|
294
|
+
limit_counter(DROPPED_LABEL_SETS_METRIC_NAME, DROPPED_LABEL_SETS_METRIC_DESC, [:name])
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def warn_label_set_limit(metric)
|
|
298
|
+
# the drop is always counted, while the log below is throttled
|
|
299
|
+
dropped_label_sets_counter.increment(labels: { name: metric.name.to_s })
|
|
300
|
+
|
|
301
|
+
warn_throttled(@label_set_limit_log_throttle, metric.name,
|
|
302
|
+
"prometheus: dropped a label set because the metric reached max_series_per_metric.",
|
|
303
|
+
name: metric.name, max_series_per_metric: metric.max_series_per_metric)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# The counter above is never throttled, only the log which comes with it:
|
|
307
|
+
# one line per record would flood the Fluentd log, and the count is in
|
|
308
|
+
# Prometheus already.
|
|
309
|
+
def warn_throttled(throttle, key, message, **details)
|
|
310
|
+
emit, suppressed = throttle.check(key)
|
|
311
|
+
return unless emit
|
|
312
|
+
|
|
313
|
+
details = details.merge(suppressed_log_count: suppressed) if suppressed > 0
|
|
314
|
+
log.warn(message, details)
|
|
168
315
|
end
|
|
169
316
|
|
|
170
317
|
def instrument_single(tag, time, record, metrics)
|
|
@@ -180,6 +327,9 @@ module Fluent
|
|
|
180
327
|
metrics.each do |metric|
|
|
181
328
|
begin
|
|
182
329
|
metric.instrument(record, expander)
|
|
330
|
+
rescue Fluent::Plugin::Prometheus::LabelSetLimitError
|
|
331
|
+
# dropping the label set is intended, so it is not an error event
|
|
332
|
+
warn_label_set_limit(metric)
|
|
183
333
|
rescue => e
|
|
184
334
|
log.warn "prometheus: failed to instrument a metric.", error_class: e.class, error: e, tag: tag, name: metric.name
|
|
185
335
|
router.emit_error_event(tag, time, record, e)
|
|
@@ -201,6 +351,9 @@ module Fluent
|
|
|
201
351
|
metrics.each do |metric|
|
|
202
352
|
begin
|
|
203
353
|
metric.instrument(record, expander)
|
|
354
|
+
rescue Fluent::Plugin::Prometheus::LabelSetLimitError
|
|
355
|
+
# dropping the label set is intended, so it is not an error event
|
|
356
|
+
warn_label_set_limit(metric)
|
|
204
357
|
rescue => e
|
|
205
358
|
log.warn "prometheus: failed to instrument a metric.", error_class: e.class, error: e, tag: tag, name: metric.name
|
|
206
359
|
router.emit_error_event(tag, time, record, e)
|
|
@@ -214,8 +367,9 @@ module Fluent
|
|
|
214
367
|
attr_reader :name
|
|
215
368
|
attr_reader :key
|
|
216
369
|
attr_reader :desc
|
|
370
|
+
attr_reader :max_series_per_metric
|
|
217
371
|
|
|
218
|
-
def initialize(element, registry, labels)
|
|
372
|
+
def initialize(element, registry, labels, opts = {})
|
|
219
373
|
['name', 'desc'].each do |key|
|
|
220
374
|
if element[key].nil?
|
|
221
375
|
raise ConfigError, "metric requires '#{key}' option"
|
|
@@ -230,6 +384,10 @@ module Fluent
|
|
|
230
384
|
@base_labels = Fluent::Plugin::Prometheus.parse_labels_elements(element)
|
|
231
385
|
@base_labels = labels.merge(@base_labels)
|
|
232
386
|
|
|
387
|
+
# <metric> overrides the limit given by the plugin
|
|
388
|
+
@max_series_per_metric = metric_limit(element, 'max_series_per_metric',
|
|
389
|
+
opts.fetch(:max_series_per_metric, DEFAULT_MAX_SERIES_PER_METRIC))
|
|
390
|
+
|
|
233
391
|
if @initialized
|
|
234
392
|
@base_initlabels = Fluent::Plugin::Prometheus.parse_initlabels_elements(element, @base_labels)
|
|
235
393
|
end
|
|
@@ -254,12 +412,31 @@ module Fluent
|
|
|
254
412
|
if v.is_a?(String)
|
|
255
413
|
label[k] = expander.expand(v)
|
|
256
414
|
else
|
|
257
|
-
label[k] = v.call(record)
|
|
415
|
+
label[k] = normalize_label_value(v.call(record))
|
|
258
416
|
end
|
|
259
417
|
end
|
|
260
418
|
label
|
|
261
419
|
end
|
|
262
420
|
|
|
421
|
+
# Instruments a record through the given block and keeps its label set
|
|
422
|
+
# as a series once the client holds it. The slot is taken before
|
|
423
|
+
# instrumenting and given back when the client refused the record, so
|
|
424
|
+
# that a label set the client does not hold does not exhaust
|
|
425
|
+
# max_series_per_metric.
|
|
426
|
+
def with_label_set(record, expander)
|
|
427
|
+
label = labels(record, expander)
|
|
428
|
+
reserved = reserve_series!(label)
|
|
429
|
+
begin
|
|
430
|
+
result = yield label
|
|
431
|
+
rescue
|
|
432
|
+
# a call which joined another's reservation has nothing to release
|
|
433
|
+
release_series(label) if reserved
|
|
434
|
+
raise
|
|
435
|
+
end
|
|
436
|
+
confirm_series(label)
|
|
437
|
+
result
|
|
438
|
+
end
|
|
439
|
+
|
|
263
440
|
def self.get(registry, name, type, docstring)
|
|
264
441
|
metric = registry.get(name)
|
|
265
442
|
|
|
@@ -273,10 +450,101 @@ module Fluent
|
|
|
273
450
|
|
|
274
451
|
metric
|
|
275
452
|
end
|
|
453
|
+
|
|
454
|
+
# <initlabels> label sets go to the client as soon as the
|
|
455
|
+
# configuration is read. Every <metric> section with the same name
|
|
456
|
+
# shares them, so reject a section which has no room for the label
|
|
457
|
+
# sets.
|
|
458
|
+
def check_series_limit!
|
|
459
|
+
return if @max_series_per_metric <= 0
|
|
460
|
+
# two <initlabels> blocks with the same values make one label set
|
|
461
|
+
held = @series_set.initial_size
|
|
462
|
+
return if held <= @max_series_per_metric
|
|
463
|
+
|
|
464
|
+
raise ConfigError, "metric #{@name} already holds #{held} label sets from <initlabels>, " \
|
|
465
|
+
"shared by every <metric> section with this name, " \
|
|
466
|
+
"but max_series_per_metric is #{@max_series_per_metric} in this section: " \
|
|
467
|
+
"the limit is already exceeded before any record arrives"
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
private
|
|
471
|
+
|
|
472
|
+
# The client refuses a value which is not a number, but it is only
|
|
473
|
+
# called once the label set has been reserved, and Summary refuses it
|
|
474
|
+
# only once it has already incremented its count: releasing the slot
|
|
475
|
+
# does not take that half instrumented series back from the client.
|
|
476
|
+
# Refuse the value before it reaches either.
|
|
477
|
+
def validate_value!(value)
|
|
478
|
+
return if value.is_a?(Numeric)
|
|
479
|
+
|
|
480
|
+
raise ArgumentError, 'value must be a number'
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def metric_limit(element, name, default)
|
|
484
|
+
return default unless element.has_key?(name)
|
|
485
|
+
|
|
486
|
+
begin
|
|
487
|
+
# base 10 explicitly, so that a value like 08 is not an octal
|
|
488
|
+
Integer(element[name], 10)
|
|
489
|
+
rescue ArgumentError, TypeError
|
|
490
|
+
raise ConfigError, "#{name} in <metric> must be an integer: #{element[name]}"
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Called by a subclass, once it has its client metric.
|
|
495
|
+
def bind_series_set(client_metric)
|
|
496
|
+
@series_set = SeriesSet.of(client_metric)
|
|
497
|
+
|
|
498
|
+
return unless @initialized
|
|
499
|
+
|
|
500
|
+
# The client gets them even when this section has no limit, so they
|
|
501
|
+
# take their slots in both cases. A section with the same name shares
|
|
502
|
+
# this set and has to see them. Their number is fixed by the
|
|
503
|
+
# configuration. Counting them cannot leak like the label sets that
|
|
504
|
+
# records bring.
|
|
505
|
+
@base_initlabels.each do |initlabels|
|
|
506
|
+
@series_set.confirm_initial(normalize_label_set(initlabels))
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# The SeriesSet keys a label set by its values, so the same value has to
|
|
511
|
+
# look the same whether a RecordAccessor or <initlabels> produced it.
|
|
512
|
+
def normalize_label_value(value)
|
|
513
|
+
value.is_a?(String) ? value : value.to_s
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def normalize_label_set(label)
|
|
517
|
+
label.each_with_object({}) do |(k, v), normalized|
|
|
518
|
+
normalized[k] = normalize_label_value(v)
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Returns true when this call took the slot, which tells #with_label_set
|
|
523
|
+
# whether it has something to give back on failure.
|
|
524
|
+
def reserve_series!(label)
|
|
525
|
+
# If the limit is off, a label set from a record is not counted.
|
|
526
|
+
# If it is counted, the set grows with every new label set and
|
|
527
|
+
# uses too much memory.
|
|
528
|
+
return false if @max_series_per_metric <= 0
|
|
529
|
+
|
|
530
|
+
@series_set.reserve(label, @max_series_per_metric, @name)
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def confirm_series(label)
|
|
534
|
+
return if @max_series_per_metric <= 0
|
|
535
|
+
|
|
536
|
+
@series_set.confirm(label)
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def release_series(label)
|
|
540
|
+
return if @max_series_per_metric <= 0
|
|
541
|
+
|
|
542
|
+
@series_set.release(label)
|
|
543
|
+
end
|
|
276
544
|
end
|
|
277
545
|
|
|
278
546
|
class Gauge < Metric
|
|
279
|
-
def initialize(element, registry, labels)
|
|
547
|
+
def initialize(element, registry, labels, opts = {})
|
|
280
548
|
super
|
|
281
549
|
if @key.nil?
|
|
282
550
|
raise ConfigError, "gauge metric requires 'key' option"
|
|
@@ -287,6 +555,7 @@ module Fluent
|
|
|
287
555
|
rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
|
|
288
556
|
@gauge = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :gauge, element['desc'])
|
|
289
557
|
end
|
|
558
|
+
bind_series_set(@gauge)
|
|
290
559
|
|
|
291
560
|
if @initialized
|
|
292
561
|
Fluent::Plugin::Prometheus::Metric.init_label_set(@gauge, @base_initlabels, @base_labels)
|
|
@@ -300,19 +569,23 @@ module Fluent
|
|
|
300
569
|
value = @key.call(record)
|
|
301
570
|
end
|
|
302
571
|
if value
|
|
303
|
-
|
|
572
|
+
validate_value!(value)
|
|
573
|
+
with_label_set(record, expander) do |label|
|
|
574
|
+
@gauge.set(value, labels: label)
|
|
575
|
+
end
|
|
304
576
|
end
|
|
305
577
|
end
|
|
306
578
|
end
|
|
307
579
|
|
|
308
580
|
class Counter < Metric
|
|
309
|
-
def initialize(element, registry, labels)
|
|
581
|
+
def initialize(element, registry, labels, opts = {})
|
|
310
582
|
super
|
|
311
583
|
begin
|
|
312
584
|
@counter = registry.counter(element['name'].to_sym, docstring: element['desc'], labels: @base_labels.keys)
|
|
313
585
|
rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
|
|
314
586
|
@counter = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :counter, element['desc'])
|
|
315
587
|
end
|
|
588
|
+
bind_series_set(@counter)
|
|
316
589
|
|
|
317
590
|
if @initialized
|
|
318
591
|
Fluent::Plugin::Prometheus::Metric.init_label_set(@counter, @base_initlabels, @base_labels)
|
|
@@ -332,12 +605,15 @@ module Fluent
|
|
|
332
605
|
# ignore if record value is nil
|
|
333
606
|
return if value.nil?
|
|
334
607
|
|
|
335
|
-
|
|
608
|
+
validate_value!(value)
|
|
609
|
+
with_label_set(record, expander) do |label|
|
|
610
|
+
@counter.increment(by: value, labels: label)
|
|
611
|
+
end
|
|
336
612
|
end
|
|
337
613
|
end
|
|
338
614
|
|
|
339
615
|
class Summary < Metric
|
|
340
|
-
def initialize(element, registry, labels)
|
|
616
|
+
def initialize(element, registry, labels, opts = {})
|
|
341
617
|
super
|
|
342
618
|
if @key.nil?
|
|
343
619
|
raise ConfigError, "summary metric requires 'key' option"
|
|
@@ -348,6 +624,7 @@ module Fluent
|
|
|
348
624
|
rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
|
|
349
625
|
@summary = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :summary, element['desc'])
|
|
350
626
|
end
|
|
627
|
+
bind_series_set(@summary)
|
|
351
628
|
|
|
352
629
|
if @initialized
|
|
353
630
|
Fluent::Plugin::Prometheus::Metric.init_label_set(@summary, @base_initlabels, @base_labels)
|
|
@@ -361,13 +638,16 @@ module Fluent
|
|
|
361
638
|
value = @key.call(record)
|
|
362
639
|
end
|
|
363
640
|
if value
|
|
364
|
-
|
|
641
|
+
validate_value!(value)
|
|
642
|
+
with_label_set(record, expander) do |label|
|
|
643
|
+
@summary.observe(value, labels: label)
|
|
644
|
+
end
|
|
365
645
|
end
|
|
366
646
|
end
|
|
367
647
|
end
|
|
368
648
|
|
|
369
649
|
class Histogram < Metric
|
|
370
|
-
def initialize(element, registry, labels)
|
|
650
|
+
def initialize(element, registry, labels, opts = {})
|
|
371
651
|
super
|
|
372
652
|
if @key.nil?
|
|
373
653
|
raise ConfigError, "histogram metric requires 'key' option"
|
|
@@ -385,6 +665,7 @@ module Fluent
|
|
|
385
665
|
rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
|
|
386
666
|
@histogram = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :histogram, element['desc'])
|
|
387
667
|
end
|
|
668
|
+
bind_series_set(@histogram)
|
|
388
669
|
|
|
389
670
|
if @initialized
|
|
390
671
|
Fluent::Plugin::Prometheus::Metric.init_label_set(@histogram, @base_initlabels, @base_labels)
|
|
@@ -398,7 +679,10 @@ module Fluent
|
|
|
398
679
|
value = @key.call(record)
|
|
399
680
|
end
|
|
400
681
|
if value
|
|
401
|
-
|
|
682
|
+
validate_value!(value)
|
|
683
|
+
with_label_set(record, expander) do |label|
|
|
684
|
+
@histogram.observe(value, labels: label)
|
|
685
|
+
end
|
|
402
686
|
end
|
|
403
687
|
end
|
|
404
688
|
end
|
|
@@ -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
|