phihos-fluent-plugin-prometheus 2.0.3

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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/linux.yml +34 -0
  3. data/.gitignore +16 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +14 -0
  6. data/ChangeLog +43 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE +202 -0
  9. data/README.md +537 -0
  10. data/Rakefile +7 -0
  11. data/fluent-plugin-prometheus.gemspec +22 -0
  12. data/lib/fluent/plugin/filter_prometheus.rb +43 -0
  13. data/lib/fluent/plugin/in_prometheus/async_wrapper.rb +47 -0
  14. data/lib/fluent/plugin/in_prometheus.rb +230 -0
  15. data/lib/fluent/plugin/in_prometheus_monitor.rb +107 -0
  16. data/lib/fluent/plugin/in_prometheus_output_monitor.rb +234 -0
  17. data/lib/fluent/plugin/in_prometheus_tail_monitor.rb +98 -0
  18. data/lib/fluent/plugin/out_prometheus.rb +42 -0
  19. data/lib/fluent/plugin/prometheus/data_store.rb +93 -0
  20. data/lib/fluent/plugin/prometheus/placeholder_expander.rb +132 -0
  21. data/lib/fluent/plugin/prometheus.rb +418 -0
  22. data/lib/fluent/plugin/prometheus_metrics.rb +77 -0
  23. data/misc/fluentd_sample.conf +170 -0
  24. data/misc/nginx_proxy.conf +22 -0
  25. data/misc/prometheus.yaml +13 -0
  26. data/misc/prometheus_alerts.yaml +59 -0
  27. data/spec/fluent/plugin/filter_prometheus_spec.rb +118 -0
  28. data/spec/fluent/plugin/in_prometheus_monitor_spec.rb +42 -0
  29. data/spec/fluent/plugin/in_prometheus_spec.rb +225 -0
  30. data/spec/fluent/plugin/in_prometheus_tail_monitor_spec.rb +42 -0
  31. data/spec/fluent/plugin/out_prometheus_spec.rb +139 -0
  32. data/spec/fluent/plugin/prometheus/placeholder_expander_spec.rb +110 -0
  33. data/spec/fluent/plugin/prometheus_metrics_spec.rb +138 -0
  34. data/spec/fluent/plugin/shared.rb +248 -0
  35. data/spec/spec_helper.rb +10 -0
  36. metadata +176 -0
@@ -0,0 +1,42 @@
1
+ require 'fluent/plugin/output'
2
+ require 'fluent/plugin/prometheus'
3
+
4
+ module Fluent::Plugin
5
+ class PrometheusOutput < Fluent::Plugin::Output
6
+ Fluent::Plugin.register_output('prometheus', self)
7
+ include Fluent::Plugin::PrometheusLabelParser
8
+ include Fluent::Plugin::Prometheus
9
+
10
+ helpers :thread
11
+
12
+ def initialize
13
+ super
14
+ @registry = ::Prometheus::Client.registry
15
+ end
16
+
17
+ def multi_workers_ready?
18
+ true
19
+ end
20
+
21
+ def configure(conf)
22
+ super
23
+ labels = parse_labels_elements(conf)
24
+ @metrics = Fluent::Plugin::Prometheus.parse_metrics_elements(conf, @registry, labels)
25
+ end
26
+
27
+ def start
28
+ super
29
+ Fluent::Plugin::Prometheus.start_retention_threads(
30
+ @metrics,
31
+ @registry,
32
+ method(:thread_create),
33
+ method(:thread_current_running?),
34
+ @log
35
+ )
36
+ end
37
+
38
+ def process(tag, es)
39
+ instrument(tag, es, @metrics)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,93 @@
1
+ # The default Prometheus client data store has no means of removing values.
2
+ # For the "retention" feature we need to be able to remove metrics with specific labels after some time of inactivity.
3
+ # By patching the Metric class and using our own DataStore we implement that missing feature.
4
+ module Prometheus
5
+ module Client
6
+ class Metric
7
+ def remove(labels)
8
+ label_set = label_set_for(labels)
9
+ @store.remove(labels: label_set)
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ module Fluent
16
+ module Plugin
17
+ module Prometheus
18
+ # Stores all the data in simple hashes, one per metric. Each of these metrics
19
+ # synchronizes access to their hash, but multiple metrics can run observations
20
+ # concurrently.
21
+ class DataStore
22
+ class InvalidStoreSettingsError < StandardError; end
23
+ DEFAULT_METRIC_SETTINGS = { topk: 0 }
24
+
25
+ def for_metric(metric_name, metric_type:, metric_settings: {})
26
+ settings = DEFAULT_METRIC_SETTINGS.merge(metric_settings)
27
+ validate_metric_settings(metric_settings: settings)
28
+ MetricStore.new(metric_settings: settings)
29
+ end
30
+
31
+ private
32
+
33
+ def validate_metric_settings(metric_settings:)
34
+ unless metric_settings.has_key?(:topk) &&
35
+ (metric_settings[:topk].is_a? Integer) &&
36
+ metric_settings[:topk] >= 0
37
+ raise InvalidStoreSettingsError,
38
+ "Metrics need a valid :topk key"
39
+ end
40
+ end
41
+
42
+ class MetricStore
43
+ def initialize(metric_settings:)
44
+ @internal_store = Hash.new { |hash, key| hash[key] = 0.0 }
45
+ @topk = metric_settings[:topk]
46
+ @lock = Monitor.new
47
+ end
48
+
49
+ def synchronize
50
+ @lock.synchronize { yield }
51
+ end
52
+
53
+ def set(labels:, val:)
54
+ synchronize do
55
+ @internal_store[labels] = val.to_f
56
+ end
57
+ end
58
+
59
+ def increment(labels:, by: 1)
60
+ synchronize do
61
+ @internal_store[labels] += by
62
+ end
63
+ end
64
+
65
+ def get(labels:)
66
+ synchronize do
67
+ @internal_store[labels]
68
+ end
69
+ end
70
+
71
+ def remove(labels:)
72
+ synchronize do
73
+ @internal_store.delete(labels)
74
+ end
75
+ end
76
+
77
+ def all_values
78
+ synchronize do
79
+ store = @internal_store.dup
80
+ if @topk > 0
81
+ store.sort_by { |_, value| -value }.first(@topk).to_h
82
+ else
83
+ store
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ private_constant :MetricStore
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,132 @@
1
+ module Fluent
2
+ module Plugin
3
+ module Prometheus
4
+ class ExpandBuilder
5
+ def self.build(placeholder, log:)
6
+ new(log: log).build(placeholder)
7
+ end
8
+
9
+ def initialize(log:)
10
+ @log = log
11
+ end
12
+
13
+ def build(placeholder_values)
14
+ placeholders = {}
15
+ placeholder_values.each do |key, value|
16
+ case value
17
+ when Array
18
+ size = value.size
19
+ value.each_with_index do |v, i|
20
+ placeholders["${#{key}[#{i}]}"] = v
21
+ placeholders["${#{key}[#{i - size}]}"] = v
22
+ end
23
+ when Hash
24
+ value.each do |k, v|
25
+ placeholders[%(${#{key}["#{k}"]})] = v
26
+ end
27
+ else
28
+ if key == 'tag'
29
+ placeholders.merge!(build_tag(value))
30
+ else
31
+ placeholders["${#{key}}"] = value
32
+ end
33
+ end
34
+ end
35
+
36
+ Fluent::Plugin::Prometheus::ExpandBuilder::PlaceholderExpander.new(@log, placeholders)
37
+ end
38
+
39
+ private
40
+
41
+ def build_tag(tag)
42
+ tags = tag.split('.')
43
+
44
+ placeholders = { '${tag}' => tag }
45
+
46
+ size = tags.size
47
+
48
+ tags.each_with_index do |v, i|
49
+ placeholders["${tag_parts[#{i}]}"] = v
50
+ placeholders["${tag_parts[#{i - size}]}"] = v
51
+ end
52
+
53
+ tag_prefix(tags).each_with_index do |v, i|
54
+ placeholders["${tag_prefix[#{i}]}"] = v
55
+ end
56
+
57
+ tag_suffix(tags).each_with_index do |v, i|
58
+ placeholders["${tag_suffix[#{i}]}"] = v
59
+ end
60
+
61
+ placeholders
62
+ end
63
+
64
+ def tag_prefix(tags)
65
+ tags = tags.dup
66
+ return [] if tags.empty?
67
+
68
+ ret = [tags.shift]
69
+ tags.each.with_index(1) do |tag, i|
70
+ ret[i] = "#{ret[i-1]}.#{tag}"
71
+ end
72
+ ret
73
+ end
74
+
75
+ def tag_suffix(tags)
76
+ return [] if tags.empty?
77
+
78
+ tags = tags.dup.reverse
79
+ ret = [tags.shift]
80
+ tags.each.with_index(1) do |tag, i|
81
+ ret[i] = "#{tag}.#{ret[i-1]}"
82
+ end
83
+ ret
84
+ end
85
+
86
+ class PlaceholderExpander
87
+ PLACEHOLDER_REGEX = /(\${[^\[}]+(\[[^\]]+\])?})/.freeze
88
+
89
+ attr_reader :placeholder
90
+
91
+ def initialize(log, placeholder)
92
+ @placeholder = placeholder
93
+ @log = log
94
+ @expander_cache = {}
95
+ end
96
+
97
+ def merge_placeholder(placeholder)
98
+ @placeholder.merge!(placeholder)
99
+ end
100
+
101
+ def expand(str, dynamic_placeholders: nil)
102
+ expander = if dynamic_placeholders
103
+ if @expander_cache[dynamic_placeholders]
104
+ @expander_cache[dynamic_placeholders]
105
+ else
106
+ e = ExpandBuilder.build(dynamic_placeholders, log: @log)
107
+ e.merge_placeholder(@placeholder)
108
+ @expander_cache[dynamic_placeholders] = e
109
+ e
110
+ end
111
+ else
112
+ self
113
+ end
114
+
115
+ expander.expand!(str)
116
+ end
117
+
118
+ protected
119
+
120
+ def expand!(str)
121
+ str.gsub(PLACEHOLDER_REGEX) { |value|
122
+ @placeholder.fetch(value) do
123
+ @log.warn("unknown placeholder `#{value}` found")
124
+ value # return as it is
125
+ end
126
+ }
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,418 @@
1
+ require 'prometheus/client'
2
+ require 'prometheus/client/formats/text'
3
+ require 'fluent/plugin/prometheus/placeholder_expander'
4
+ require 'fluent/plugin/prometheus/data_store'
5
+
6
+ module Fluent
7
+ module Plugin
8
+ module PrometheusLabelParser
9
+ def configure(conf)
10
+ super
11
+ # Check if running with multiple workers
12
+ sysconf = if self.respond_to?(:owner) && owner.respond_to?(:system_config)
13
+ owner.system_config
14
+ elsif self.respond_to?(:system_config)
15
+ self.system_config
16
+ else
17
+ nil
18
+ end
19
+ @multi_worker = sysconf && sysconf.workers ? (sysconf.workers > 1) : false
20
+ end
21
+
22
+ def parse_labels_elements(conf)
23
+ base_labels = Fluent::Plugin::Prometheus.parse_labels_elements(conf)
24
+
25
+ if @multi_worker
26
+ base_labels[:worker_id] = fluentd_worker_id.to_s
27
+ end
28
+
29
+ base_labels
30
+ end
31
+ end
32
+
33
+ module Prometheus
34
+ class AlreadyRegisteredError < StandardError; end
35
+
36
+ def self.parse_labels_elements(conf)
37
+ labels = conf.elements.select { |e| e.name == 'labels' }
38
+ if labels.size > 1
39
+ raise ConfigError, "labels section must have at most 1"
40
+ end
41
+
42
+ base_labels = {}
43
+ unless labels.empty?
44
+ labels.first.each do |key, value|
45
+ labels.first.has_key?(key)
46
+
47
+ # use RecordAccessor only for $. and $[ syntax
48
+ # otherwise use the value as is or expand the value by RecordTransformer for ${} syntax
49
+ if value.start_with?('$.') || value.start_with?('$[')
50
+ base_labels[key.to_sym] = PluginHelper::RecordAccessor::Accessor.new(value)
51
+ else
52
+ base_labels[key.to_sym] = value
53
+ end
54
+ end
55
+ end
56
+
57
+ base_labels
58
+ end
59
+
60
+ def self.parse_metrics_elements(conf, registry, labels = {})
61
+ metrics = []
62
+ conf.elements.select { |element|
63
+ element.name == 'metric'
64
+ }.each { |element|
65
+ if element.has_key?('key') && (element['key'].start_with?('$.') || element['key'].start_with?('$['))
66
+ value = element['key']
67
+ element['key'] = PluginHelper::RecordAccessor::Accessor.new(value)
68
+ end
69
+ case element['type']
70
+ when 'summary'
71
+ metrics << Fluent::Plugin::Prometheus::Summary.new(element, registry, labels)
72
+ when 'gauge'
73
+ metrics << Fluent::Plugin::Prometheus::Gauge.new(element, registry, labels)
74
+ when 'counter'
75
+ metrics << Fluent::Plugin::Prometheus::Counter.new(element, registry, labels)
76
+ when 'histogram'
77
+ metrics << Fluent::Plugin::Prometheus::Histogram.new(element, registry, labels)
78
+ else
79
+ raise ConfigError, "type option must be 'counter', 'gauge', 'summary' or 'histogram'"
80
+ end
81
+ }
82
+ metrics
83
+ end
84
+
85
+ def self.start_retention_threads(metrics, registry, thread_create, thread_running, log)
86
+ metrics.select { |metric| metric.has_retention? }.each do |metric|
87
+ thread_create.call("prometheus_retention_#{metric.name}".to_sym) do
88
+ while thread_running.call()
89
+ metric.remove_expired_metrics(registry, log)
90
+ sleep(metric.retention_check_interval)
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ def self.placeholder_expander(log)
97
+ Fluent::Plugin::Prometheus::ExpandBuilder.new(log: log)
98
+ end
99
+
100
+ def stringify_keys(hash_to_stringify)
101
+ # Adapted from: https://www.jvt.me/posts/2019/09/07/ruby-hash-keys-string-symbol/
102
+ hash_to_stringify.map do |k,v|
103
+ value_or_hash = if v.instance_of? Hash
104
+ stringify_keys(v)
105
+ else
106
+ v
107
+ end
108
+ [k.to_s, value_or_hash]
109
+ end.to_h
110
+ end
111
+
112
+ def initialize
113
+ super
114
+ ::Prometheus::Client.config.data_store = Fluent::Plugin::Prometheus::DataStore.new
115
+ end
116
+
117
+ def configure(conf)
118
+ super
119
+ @placeholder_values = {}
120
+ @placeholder_expander_builder = Fluent::Plugin::Prometheus.placeholder_expander(log)
121
+ @hostname = Socket.gethostname
122
+ end
123
+
124
+ def instrument_single(tag, time, record, metrics)
125
+ @placeholder_values[tag] ||= {
126
+ 'tag' => tag,
127
+ 'hostname' => @hostname,
128
+ 'worker_id' => fluentd_worker_id,
129
+ }
130
+
131
+ record = stringify_keys(record)
132
+ placeholders = record.merge(@placeholder_values[tag])
133
+ expander = @placeholder_expander_builder.build(placeholders)
134
+ metrics.each do |metric|
135
+ begin
136
+ metric.instrument(record, expander)
137
+ rescue => e
138
+ log.warn "prometheus: failed to instrument a metric.", error_class: e.class, error: e, tag: tag, name: metric.name
139
+ router.emit_error_event(tag, time, record, e)
140
+ end
141
+ end
142
+ end
143
+
144
+ def instrument(tag, es, metrics)
145
+ placeholder_values = {
146
+ 'tag' => tag,
147
+ 'hostname' => @hostname,
148
+ 'worker_id' => fluentd_worker_id,
149
+ }
150
+
151
+ es.each do |time, record|
152
+ record = stringify_keys(record)
153
+ placeholders = record.merge(placeholder_values)
154
+ expander = @placeholder_expander_builder.build(placeholders)
155
+ metrics.each do |metric|
156
+ begin
157
+ metric.instrument(record, expander)
158
+ rescue => e
159
+ log.warn "prometheus: failed to instrument a metric.", error_class: e.class, error: e, tag: tag, name: metric.name
160
+ router.emit_error_event(tag, time, record, e)
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ class Metric
167
+ attr_reader :type
168
+ attr_reader :name
169
+ attr_reader :key
170
+ attr_reader :desc
171
+ attr_reader :retention
172
+ attr_reader :retention_check_interval
173
+
174
+ def initialize(element, registry, labels)
175
+ ['name', 'desc'].each do |key|
176
+ if element[key].nil?
177
+ raise ConfigError, "metric requires '#{key}' option"
178
+ end
179
+ end
180
+ @type = element['type']
181
+ @name = element['name']
182
+ @key = element['key']
183
+ @desc = element['desc']
184
+ @retention = element['retention'].to_i
185
+ @retention_check_interval = element.fetch('retention_check_interval', 60).to_i
186
+ if has_retention?
187
+ @last_modified_store = LastModifiedStore.new
188
+ end
189
+ @topk = element['topk'].to_i
190
+
191
+ @base_labels = Fluent::Plugin::Prometheus.parse_labels_elements(element)
192
+ @base_labels = labels.merge(@base_labels)
193
+ end
194
+
195
+ def labels(record, expander)
196
+ label = {}
197
+ @base_labels.each do |k, v|
198
+ if v.is_a?(String)
199
+ label[k] = expander.expand(v)
200
+ else
201
+ label[k] = v.call(record)
202
+ end
203
+ end
204
+ label
205
+ end
206
+
207
+ def self.get(registry, name, type, docstring)
208
+ metric = registry.get(name)
209
+
210
+ # should have same type, docstring
211
+ if metric.type != type
212
+ raise AlreadyRegisteredError, "#{name} has already been registered as #{type} type"
213
+ end
214
+ if metric.docstring != docstring
215
+ raise AlreadyRegisteredError, "#{name} has already been registered with different docstring"
216
+ end
217
+
218
+ metric
219
+ end
220
+
221
+ def set_value?(value)
222
+ if value
223
+ return true
224
+ end
225
+ false
226
+ end
227
+
228
+ def instrument(record, expander)
229
+ value = self.value(record)
230
+ if self.set_value?(value)
231
+ labels = labels(record, expander)
232
+ set_value(value, labels)
233
+ if has_retention?
234
+ @last_modified_store.set_last_updated(labels)
235
+ end
236
+ end
237
+ end
238
+
239
+ def has_retention?
240
+ @retention > 0
241
+ end
242
+
243
+ def remove_expired_metrics(registry, log)
244
+ if has_retention?
245
+ metric = registry.get(@name)
246
+
247
+ expiration_time = Time.now - @retention
248
+ expired_label_sets = @last_modified_store.get_labels_not_modified_since(expiration_time)
249
+
250
+ expired_label_sets.each { |expired_label_set|
251
+ log.debug "Metric #{@name} with labels #{expired_label_set} expired. Removing..."
252
+ metric.remove(expired_label_set) # this method is supplied by the require at the top of this method
253
+ @last_modified_store.remove(expired_label_set)
254
+ }
255
+ else
256
+ log.warn('remove_expired_metrics should not be called when retention is not set for this metric!')
257
+ end
258
+ end
259
+
260
+ class LastModifiedStore
261
+ def initialize
262
+ @internal_store = Hash.new
263
+ @lock = Monitor.new
264
+ end
265
+
266
+ def synchronize
267
+ @lock.synchronize { yield }
268
+ end
269
+
270
+ def set_last_updated(labels)
271
+ synchronize do
272
+ @internal_store[labels] = Time.now
273
+ end
274
+ end
275
+
276
+ def remove(labels)
277
+ synchronize do
278
+ @internal_store.delete(labels)
279
+ end
280
+ end
281
+
282
+ def get_labels_not_modified_since(time)
283
+ synchronize do
284
+ @internal_store.select { |k, v| v < time }.keys
285
+ end
286
+ end
287
+ end
288
+ end
289
+
290
+ class Gauge < Metric
291
+ def initialize(element, registry, labels)
292
+ super
293
+ if @key.nil?
294
+ raise ConfigError, "gauge metric requires 'key' option"
295
+ end
296
+
297
+ begin
298
+ @gauge = registry.gauge(
299
+ element['name'].to_sym,
300
+ docstring: element['desc'],
301
+ labels: @base_labels.keys,
302
+ store_settings: { topk: @topk }
303
+ )
304
+ rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
305
+ @gauge = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :gauge, element['desc'])
306
+ end
307
+ end
308
+
309
+ def value(record)
310
+ if @key.is_a?(String)
311
+ record[@key]
312
+ else
313
+ @key.call(record)
314
+ end
315
+ end
316
+
317
+ def set_value(value, labels)
318
+ @gauge.set(value, labels: labels)
319
+ end
320
+ end
321
+
322
+ class Counter < Metric
323
+ def initialize(element, registry, labels)
324
+ super
325
+ begin
326
+ @counter = registry.counter(
327
+ element['name'].to_sym,
328
+ docstring: element['desc'],
329
+ labels: @base_labels.keys,
330
+ store_settings: { topk: @topk }
331
+ )
332
+ rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
333
+ @counter = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :counter, element['desc'])
334
+ end
335
+ end
336
+
337
+ def value(record)
338
+ if @key.nil?
339
+ 1
340
+ elsif @key.is_a?(String)
341
+ record[@key]
342
+ else
343
+ @key.call(record)
344
+ end
345
+ end
346
+
347
+ def set_value?(value)
348
+ !value.nil?
349
+ end
350
+
351
+ def set_value(value, labels)
352
+ @counter.increment(by: value, labels: labels)
353
+ end
354
+ end
355
+
356
+ class Summary < Metric
357
+ def initialize(element, registry, labels)
358
+ super
359
+ if @key.nil?
360
+ raise ConfigError, "summary metric requires 'key' option"
361
+ end
362
+
363
+ begin
364
+ @summary = registry.summary(element['name'].to_sym, docstring: element['desc'], labels: @base_labels.keys)
365
+ rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
366
+ @summary = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :summary, element['desc'])
367
+ end
368
+ end
369
+
370
+ def value(record)
371
+ if @key.is_a?(String)
372
+ record[@key]
373
+ else
374
+ @key.call(record)
375
+ end
376
+ end
377
+
378
+ def set_value(value, labels)
379
+ @summary.observe(value, labels: labels)
380
+ end
381
+ end
382
+
383
+ class Histogram < Metric
384
+ def initialize(element, registry, labels)
385
+ super
386
+ if @key.nil?
387
+ raise ConfigError, "histogram metric requires 'key' option"
388
+ end
389
+
390
+ begin
391
+ if element['buckets']
392
+ buckets = element['buckets'].split(/,/).map(&:strip).map do |e|
393
+ e[/\A\d+.\d+\Z/] ? e.to_f : e.to_i
394
+ end
395
+ @histogram = registry.histogram(element['name'].to_sym, docstring: element['desc'], labels: @base_labels.keys, buckets: buckets)
396
+ else
397
+ @histogram = registry.histogram(element['name'].to_sym, docstring: element['desc'], labels: @base_labels.keys)
398
+ end
399
+ rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
400
+ @histogram = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :histogram, element['desc'])
401
+ end
402
+ end
403
+
404
+ def value(record)
405
+ if @key.is_a?(String)
406
+ record[@key]
407
+ else
408
+ @key.call(record)
409
+ end
410
+ end
411
+
412
+ def set_value(value, labels)
413
+ @histogram.observe(value, labels: labels)
414
+ end
415
+ end
416
+ end
417
+ end
418
+ end