fluent-plugin-prometheus 1.7.0 → 2.0.2

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.
@@ -0,0 +1,47 @@
1
+ require 'async'
2
+
3
+ module Fluent::Plugin
4
+ class PrometheusInput
5
+ module AsyncWrapper
6
+ def do_request(host:, port:, secure:)
7
+ endpoint =
8
+ if secure
9
+ context = OpenSSL::SSL::SSLContext.new
10
+ context.verify_mode = OpenSSL::SSL::VERIFY_NONE
11
+ Async::HTTP::Endpoint.parse("https://#{host}:#{port}", ssl_context: context)
12
+ else
13
+ Async::HTTP::Endpoint.parse("http://#{host}:#{port}")
14
+ end
15
+
16
+ Async::HTTP::Client.open(endpoint) do |client|
17
+ yield(AsyncHttpWrapper.new(client))
18
+ end
19
+ end
20
+
21
+ Response = Struct.new(:code, :body, :headers)
22
+
23
+ class AsyncHttpWrapper
24
+ def initialize(http)
25
+ @http = http
26
+ end
27
+
28
+ def get(path)
29
+ error = nil
30
+ response = Async::Task.current.async {
31
+ begin
32
+ @http.get(path)
33
+ rescue => e # Async::Reactor rescue all error. handle it by itself
34
+ error = e
35
+ end
36
+ }.wait
37
+
38
+ if error
39
+ raise error
40
+ end
41
+
42
+ Response.new(response.status.to_s, response.read || '', response.headers)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -24,14 +24,14 @@ module Fluent::Plugin
24
24
  def configure(conf)
25
25
  super
26
26
  hostname = Socket.gethostname
27
- expander = Fluent::Plugin::Prometheus.placeholder_expander(log)
28
- placeholders = expander.prepare_placeholders({'hostname' => hostname, 'worker_id' => fluentd_worker_id})
27
+ expander_builder = Fluent::Plugin::Prometheus.placeholder_expander(log)
28
+ expander = expander_builder.build({ 'hostname' => hostname, 'worker_id' => fluentd_worker_id })
29
29
  @base_labels = parse_labels_elements(conf)
30
30
  @base_labels.each do |key, value|
31
31
  unless value.is_a?(String)
32
32
  raise Fluent::ConfigError, "record accessor syntax is not available in prometheus_monitor"
33
33
  end
34
- @base_labels[key] = expander.expand(value, placeholders)
34
+ @base_labels[key] = expander.expand(value)
35
35
  end
36
36
 
37
37
  if defined?(Fluent::Plugin) && defined?(Fluent::Plugin::MonitorAgentInput)
@@ -46,19 +46,19 @@ module Fluent::Plugin
46
46
  def start
47
47
  super
48
48
 
49
- @buffer_newest_timekey = @registry.gauge(
49
+ @buffer_newest_timekey = get_gauge(
50
50
  :fluentd_status_buffer_newest_timekey,
51
51
  'Newest timekey in buffer.')
52
- @buffer_oldest_timekey = @registry.gauge(
52
+ @buffer_oldest_timekey = get_gauge(
53
53
  :fluentd_status_buffer_oldest_timekey,
54
54
  'Oldest timekey in buffer.')
55
- buffer_queue_length = @registry.gauge(
55
+ buffer_queue_length = get_gauge(
56
56
  :fluentd_status_buffer_queue_length,
57
57
  'Current buffer queue length.')
58
- buffer_total_queued_size = @registry.gauge(
58
+ buffer_total_queued_size = get_gauge(
59
59
  :fluentd_status_buffer_total_bytes,
60
60
  'Current total size of queued buffers.')
61
- retry_counts = @registry.gauge(
61
+ retry_counts = get_gauge(
62
62
  :fluentd_status_retry_count,
63
63
  'Current retry counts.')
64
64
 
@@ -76,14 +76,14 @@ module Fluent::Plugin
76
76
 
77
77
  @monitor_info.each do |name, metric|
78
78
  if info[name]
79
- metric.set(label, info[name])
79
+ metric.set(info[name], labels: label)
80
80
  end
81
81
  end
82
82
 
83
83
  timekeys = info["buffer_timekeys"]
84
84
  if timekeys && !timekeys.empty?
85
- @buffer_newest_timekey.set(label, timekeys.max)
86
- @buffer_oldest_timekey.set(label, timekeys.min)
85
+ @buffer_newest_timekey.set(timekeys.max, labels: label)
86
+ @buffer_oldest_timekey.set(timekeys.min, labels: label)
87
87
  end
88
88
  end
89
89
  end
@@ -95,5 +95,13 @@ module Fluent::Plugin
95
95
  type: plugin_info["type"],
96
96
  )
97
97
  end
98
+
99
+ def get_gauge(name, docstring)
100
+ if @registry.exist?(name)
101
+ @registry.get(name)
102
+ else
103
+ @registry.gauge(name, docstring: docstring, labels: @base_labels.keys + [:plugin_id, :plugin_category, :type])
104
+ end
105
+ end
98
106
  end
99
107
  end
@@ -1,15 +1,16 @@
1
- require 'fluent/input'
1
+ require 'fluent/plugin/input'
2
2
  require 'fluent/plugin/in_monitor_agent'
3
3
  require 'fluent/plugin/prometheus'
4
4
 
5
5
  module Fluent::Plugin
6
- class PrometheusOutputMonitorInput < Fluent::Input
6
+ class PrometheusOutputMonitorInput < Fluent::Plugin::Input
7
7
  Fluent::Plugin.register_input('prometheus_output_monitor', self)
8
8
  include Fluent::Plugin::PrometheusLabelParser
9
9
 
10
10
  helpers :timer
11
11
 
12
12
  config_param :interval, :time, default: 5
13
+ config_param :gauge_all, :bool, default: true
13
14
  attr_reader :registry
14
15
 
15
16
  MONITOR_IVARS = [
@@ -43,22 +44,19 @@ module Fluent::Plugin
43
44
  def configure(conf)
44
45
  super
45
46
  hostname = Socket.gethostname
46
- expander = Fluent::Plugin::Prometheus.placeholder_expander(log)
47
- placeholders = expander.prepare_placeholders({'hostname' => hostname, 'worker_id' => fluentd_worker_id})
47
+ expander_builder = Fluent::Plugin::Prometheus.placeholder_expander(log)
48
+ expander = expander_builder.build({ 'hostname' => hostname, 'worker_id' => fluentd_worker_id })
48
49
  @base_labels = parse_labels_elements(conf)
49
50
  @base_labels.each do |key, value|
50
51
  unless value.is_a?(String)
51
52
  raise Fluent::ConfigError, "record accessor syntax is not available in prometheus_output_monitor"
52
53
  end
53
- @base_labels[key] = expander.expand(value, placeholders)
54
+ @base_labels[key] = expander.expand(value)
54
55
  end
55
56
 
56
- if defined?(Fluent::Plugin) && defined?(Fluent::Plugin::MonitorAgentInput)
57
- # from v0.14.6
58
- @monitor_agent = Fluent::Plugin::MonitorAgentInput.new
59
- else
60
- @monitor_agent = Fluent::MonitorAgentInput.new
61
- end
57
+ @monitor_agent = Fluent::Plugin::MonitorAgentInput.new
58
+
59
+ @gauge_or_counter = @gauge_all ? :gauge : :counter
62
60
  end
63
61
 
64
62
  def start
@@ -66,57 +64,57 @@ module Fluent::Plugin
66
64
 
67
65
  @metrics = {
68
66
  # Buffer metrics
69
- buffer_total_queued_size: @registry.gauge(
67
+ buffer_total_queued_size: get_gauge(
70
68
  :fluentd_output_status_buffer_total_bytes,
71
69
  'Current total size of stage and queue buffers.'),
72
- buffer_stage_length: @registry.gauge(
70
+ buffer_stage_length: get_gauge(
73
71
  :fluentd_output_status_buffer_stage_length,
74
72
  'Current length of stage buffers.'),
75
- buffer_stage_byte_size: @registry.gauge(
73
+ buffer_stage_byte_size: get_gauge(
76
74
  :fluentd_output_status_buffer_stage_byte_size,
77
75
  'Current total size of stage buffers.'),
78
- buffer_queue_length: @registry.gauge(
76
+ buffer_queue_length: get_gauge(
79
77
  :fluentd_output_status_buffer_queue_length,
80
78
  'Current length of queue buffers.'),
81
- buffer_queue_byte_size: @registry.gauge(
79
+ buffer_queue_byte_size: get_gauge(
82
80
  :fluentd_output_status_queue_byte_size,
83
81
  'Current total size of queue buffers.'),
84
- buffer_available_buffer_space_ratios: @registry.gauge(
82
+ buffer_available_buffer_space_ratios: get_gauge(
85
83
  :fluentd_output_status_buffer_available_space_ratio,
86
84
  'Ratio of available space in buffer.'),
87
- buffer_newest_timekey: @registry.gauge(
85
+ buffer_newest_timekey: get_gauge(
88
86
  :fluentd_output_status_buffer_newest_timekey,
89
87
  'Newest timekey in buffer.'),
90
- buffer_oldest_timekey: @registry.gauge(
88
+ buffer_oldest_timekey: get_gauge(
91
89
  :fluentd_output_status_buffer_oldest_timekey,
92
90
  'Oldest timekey in buffer.'),
93
91
 
94
92
  # Output metrics
95
- retry_counts: @registry.gauge(
93
+ retry_counts: get_gauge_or_counter(
96
94
  :fluentd_output_status_retry_count,
97
95
  'Current retry counts.'),
98
- num_errors: @registry.gauge(
96
+ num_errors: get_gauge_or_counter(
99
97
  :fluentd_output_status_num_errors,
100
98
  'Current number of errors.'),
101
- emit_count: @registry.gauge(
99
+ emit_count: get_gauge_or_counter(
102
100
  :fluentd_output_status_emit_count,
103
101
  'Current emit counts.'),
104
- emit_records: @registry.gauge(
102
+ emit_records: get_gauge_or_counter(
105
103
  :fluentd_output_status_emit_records,
106
104
  'Current emit records.'),
107
- write_count: @registry.gauge(
105
+ write_count: get_gauge_or_counter(
108
106
  :fluentd_output_status_write_count,
109
107
  'Current write counts.'),
110
- rollback_count: @registry.gauge(
108
+ rollback_count: get_gauge(
111
109
  :fluentd_output_status_rollback_count,
112
110
  'Current rollback counts.'),
113
- flush_time_count: @registry.gauge(
111
+ flush_time_count: get_gauge_or_counter(
114
112
  :fluentd_output_status_flush_time_count,
115
113
  'Total flush time.'),
116
- slow_flush_count: @registry.gauge(
114
+ slow_flush_count: get_gauge_or_counter(
117
115
  :fluentd_output_status_slow_flush_count,
118
116
  'Current slow flush counts.'),
119
- retry_wait: @registry.gauge(
117
+ retry_wait: get_gauge(
120
118
  :fluentd_output_status_retry_wait,
121
119
  'Current retry wait'),
122
120
  }
@@ -146,7 +144,16 @@ module Fluent::Plugin
146
144
 
147
145
  # output metrics
148
146
  'retry_count' => @metrics[:retry_counts],
147
+ # Needed since Fluentd v1.14 due to metrics extensions.
148
+ 'num_errors' => @metrics[:num_errors],
149
+ 'write_count' => @metrics[:write_count],
150
+ 'emit_count' => @metrics[:emit_count],
151
+ 'emit_records' => @metrics[:emit_records],
152
+ 'rollback_count' => @metrics[:rollback_count],
153
+ 'flush_time_count' => @metrics[:flush_time_count],
154
+ 'slow_flush_count' => @metrics[:slow_flush_count],
149
155
  }
156
+ # No needed for Fluentd v1.14 but leave as-is for backward compatibility.
150
157
  instance_vars_info = {
151
158
  num_errors: @metrics[:num_errors],
152
159
  write_count: @metrics[:write_count],
@@ -162,14 +169,22 @@ module Fluent::Plugin
162
169
 
163
170
  monitor_info.each do |name, metric|
164
171
  if info[name]
165
- metric.set(label, info[name])
172
+ if metric.is_a?(::Prometheus::Client::Gauge)
173
+ metric.set(info[name], labels: label)
174
+ elsif metric.is_a?(::Prometheus::Client::Counter)
175
+ metric.increment(by: info[name] - metric.get(labels: label), labels: label)
176
+ end
166
177
  end
167
178
  end
168
179
 
169
180
  if info['instance_variables']
170
181
  instance_vars_info.each do |name, metric|
171
182
  if info['instance_variables'][name]
172
- metric.set(label, info['instance_variables'][name])
183
+ if metric.is_a?(::Prometheus::Client::Gauge)
184
+ metric.set(info['instance_variables'][name], labels: label)
185
+ elsif metric.is_a?(::Prometheus::Client::Counter)
186
+ metric.increment(by: info['instance_variables'][name] - metric.get(labels: label), labels: label)
187
+ end
173
188
  end
174
189
  end
175
190
  end
@@ -187,7 +202,7 @@ module Fluent::Plugin
187
202
  if next_time && start_time
188
203
  wait = next_time - start_time
189
204
  end
190
- @metrics[:retry_wait].set(label, wait.to_f)
205
+ @metrics[:retry_wait].set(wait.to_f, labels: label)
191
206
  end
192
207
  end
193
208
  end
@@ -198,5 +213,21 @@ module Fluent::Plugin
198
213
  type: plugin_info["type"],
199
214
  )
200
215
  end
216
+
217
+ def get_gauge(name, docstring)
218
+ if @registry.exist?(name)
219
+ @registry.get(name)
220
+ else
221
+ @registry.gauge(name, docstring: docstring, labels: @base_labels.keys + [:plugin_id, :type])
222
+ end
223
+ end
224
+
225
+ def get_gauge_or_counter(name, docstring)
226
+ if @registry.exist?(name)
227
+ @registry.get(name)
228
+ else
229
+ @registry.public_send(@gauge_or_counter, name, docstring: docstring, labels: @base_labels.keys + [:plugin_id, :type])
230
+ end
231
+ end
201
232
  end
202
233
  end
@@ -28,32 +28,27 @@ module Fluent::Plugin
28
28
  def configure(conf)
29
29
  super
30
30
  hostname = Socket.gethostname
31
- expander = Fluent::Plugin::Prometheus.placeholder_expander(log)
32
- placeholders = expander.prepare_placeholders({'hostname' => hostname, 'worker_id' => fluentd_worker_id})
31
+ expander_builder = Fluent::Plugin::Prometheus.placeholder_expander(log)
32
+ expander = expander_builder.build({ 'hostname' => hostname, 'worker_id' => fluentd_worker_id })
33
33
  @base_labels = parse_labels_elements(conf)
34
34
  @base_labels.each do |key, value|
35
35
  unless value.is_a?(String)
36
36
  raise Fluent::ConfigError, "record accessor syntax is not available in prometheus_tail_monitor"
37
37
  end
38
- @base_labels[key] = expander.expand(value, placeholders)
38
+ @base_labels[key] = expander.expand(value)
39
39
  end
40
40
 
41
- if defined?(Fluent::Plugin) && defined?(Fluent::Plugin::MonitorAgentInput)
42
- # from v0.14.6
43
- @monitor_agent = Fluent::Plugin::MonitorAgentInput.new
44
- else
45
- @monitor_agent = Fluent::MonitorAgentInput.new
46
- end
41
+ @monitor_agent = Fluent::Plugin::MonitorAgentInput.new
47
42
  end
48
43
 
49
44
  def start
50
45
  super
51
46
 
52
47
  @metrics = {
53
- position: @registry.gauge(
48
+ position: get_gauge(
54
49
  :fluentd_tail_file_position,
55
50
  'Current position of file.'),
56
- inode: @registry.gauge(
51
+ inode: get_gauge(
57
52
  :fluentd_tail_file_inode,
58
53
  'Current inode of file.'),
59
54
  }
@@ -78,8 +73,8 @@ module Fluent::Plugin
78
73
  # Very fragile implementation
79
74
  pe = watcher.instance_variable_get(:@pe)
80
75
  label = labels(info, watcher.path)
81
- @metrics[:inode].set(label, pe.read_inode)
82
- @metrics[:position].set(label, pe.read_pos)
76
+ @metrics[:inode].set(pe.read_inode, labels: label)
77
+ @metrics[:position].set(pe.read_pos, labels: label)
83
78
  end
84
79
  end
85
80
  end
@@ -91,5 +86,13 @@ module Fluent::Plugin
91
86
  path: path,
92
87
  )
93
88
  end
89
+
90
+ def get_gauge(name, docstring)
91
+ if @registry.exist?(name)
92
+ @registry.get(name)
93
+ else
94
+ @registry.gauge(name, docstring: docstring, labels: @base_labels.keys + [:plugin_id, :type, :path])
95
+ end
96
+ end
94
97
  end
95
98
  end
@@ -1,6 +1,6 @@
1
1
  require 'prometheus/client'
2
2
  require 'prometheus/client/formats/text'
3
- require 'fluent/plugin/filter_record_transformer'
3
+ require 'fluent/plugin/prometheus/placeholder_expander'
4
4
 
5
5
  module Fluent
6
6
  module Plugin
@@ -82,14 +82,25 @@ module Fluent
82
82
  end
83
83
 
84
84
  def self.placeholder_expander(log)
85
- # Use internal class in order to expand placeholder
86
- Fluent::Plugin::RecordTransformerFilter::PlaceholderExpander.new(log: log)
85
+ Fluent::Plugin::Prometheus::ExpandBuilder.new(log: log)
86
+ end
87
+
88
+ def stringify_keys(hash_to_stringify)
89
+ # Adapted from: https://www.jvt.me/posts/2019/09/07/ruby-hash-keys-string-symbol/
90
+ hash_to_stringify.map do |k,v|
91
+ value_or_hash = if v.instance_of? Hash
92
+ stringify_keys(v)
93
+ else
94
+ v
95
+ end
96
+ [k.to_s, value_or_hash]
97
+ end.to_h
87
98
  end
88
99
 
89
100
  def configure(conf)
90
101
  super
91
102
  @placeholder_values = {}
92
- @placeholder_expander = Fluent::Plugin::Prometheus.placeholder_expander(log)
103
+ @placeholder_expander_builder = Fluent::Plugin::Prometheus.placeholder_expander(log)
93
104
  @hostname = Socket.gethostname
94
105
  end
95
106
 
@@ -100,11 +111,12 @@ module Fluent
100
111
  'worker_id' => fluentd_worker_id,
101
112
  }
102
113
 
114
+ record = stringify_keys(record)
103
115
  placeholders = record.merge(@placeholder_values[tag])
104
- placeholders = @placeholder_expander.prepare_placeholders(placeholders)
116
+ expander = @placeholder_expander_builder.build(placeholders)
105
117
  metrics.each do |metric|
106
118
  begin
107
- metric.instrument(record, @placeholder_expander, placeholders)
119
+ metric.instrument(record, expander)
108
120
  rescue => e
109
121
  log.warn "prometheus: failed to instrument a metric.", error_class: e.class, error: e, tag: tag, name: metric.name
110
122
  router.emit_error_event(tag, time, record, e)
@@ -120,11 +132,12 @@ module Fluent
120
132
  }
121
133
 
122
134
  es.each do |time, record|
135
+ record = stringify_keys(record)
123
136
  placeholders = record.merge(placeholder_values)
124
- placeholders = @placeholder_expander.prepare_placeholders(placeholders)
137
+ expander = @placeholder_expander_builder.build(placeholders)
125
138
  metrics.each do |metric|
126
139
  begin
127
- metric.instrument(record, @placeholder_expander, placeholders)
140
+ metric.instrument(record, expander)
128
141
  rescue => e
129
142
  log.warn "prometheus: failed to instrument a metric.", error_class: e.class, error: e, tag: tag, name: metric.name
130
143
  router.emit_error_event(tag, time, record, e)
@@ -154,11 +167,11 @@ module Fluent
154
167
  @base_labels = labels.merge(@base_labels)
155
168
  end
156
169
 
157
- def labels(record, expander, placeholders)
170
+ def labels(record, expander)
158
171
  label = {}
159
172
  @base_labels.each do |k, v|
160
173
  if v.is_a?(String)
161
- label[k] = expander.expand(v, placeholders)
174
+ label[k] = expander.expand(v)
162
175
  else
163
176
  label[k] = v.call(record)
164
177
  end
@@ -189,20 +202,20 @@ module Fluent
189
202
  end
190
203
 
191
204
  begin
192
- @gauge = registry.gauge(element['name'].to_sym, element['desc'])
205
+ @gauge = registry.gauge(element['name'].to_sym, docstring: element['desc'], labels: @base_labels.keys)
193
206
  rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
194
207
  @gauge = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :gauge, element['desc'])
195
208
  end
196
209
  end
197
210
 
198
- def instrument(record, expander, placeholders)
211
+ def instrument(record, expander)
199
212
  if @key.is_a?(String)
200
213
  value = record[@key]
201
214
  else
202
215
  value = @key.call(record)
203
216
  end
204
217
  if value
205
- @gauge.set(labels(record, expander, placeholders), value)
218
+ @gauge.set(value, labels: labels(record, expander))
206
219
  end
207
220
  end
208
221
  end
@@ -211,13 +224,13 @@ module Fluent
211
224
  def initialize(element, registry, labels)
212
225
  super
213
226
  begin
214
- @counter = registry.counter(element['name'].to_sym, element['desc'])
227
+ @counter = registry.counter(element['name'].to_sym, docstring: element['desc'], labels: @base_labels.keys)
215
228
  rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
216
229
  @counter = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :counter, element['desc'])
217
230
  end
218
231
  end
219
232
 
220
- def instrument(record, expander, placeholders)
233
+ def instrument(record, expander)
221
234
  # use record value of the key if key is specified, otherwise just increment
222
235
  if @key.nil?
223
236
  value = 1
@@ -230,7 +243,7 @@ module Fluent
230
243
  # ignore if record value is nil
231
244
  return if value.nil?
232
245
 
233
- @counter.increment(labels(record, expander, placeholders), value)
246
+ @counter.increment(by: value, labels: labels(record, expander))
234
247
  end
235
248
  end
236
249
 
@@ -242,20 +255,20 @@ module Fluent
242
255
  end
243
256
 
244
257
  begin
245
- @summary = registry.summary(element['name'].to_sym, element['desc'])
258
+ @summary = registry.summary(element['name'].to_sym, docstring: element['desc'], labels: @base_labels.keys)
246
259
  rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
247
260
  @summary = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :summary, element['desc'])
248
261
  end
249
262
  end
250
263
 
251
- def instrument(record, expander, placeholders)
264
+ def instrument(record, expander)
252
265
  if @key.is_a?(String)
253
266
  value = record[@key]
254
267
  else
255
268
  value = @key.call(record)
256
269
  end
257
270
  if value
258
- @summary.observe(labels(record, expander, placeholders), value)
271
+ @summary.observe(value, labels: labels(record, expander))
259
272
  end
260
273
  end
261
274
  end
@@ -272,23 +285,23 @@ module Fluent
272
285
  buckets = element['buckets'].split(/,/).map(&:strip).map do |e|
273
286
  e[/\A\d+.\d+\Z/] ? e.to_f : e.to_i
274
287
  end
275
- @histogram = registry.histogram(element['name'].to_sym, element['desc'], {}, buckets)
288
+ @histogram = registry.histogram(element['name'].to_sym, docstring: element['desc'], labels: @base_labels.keys, buckets: buckets)
276
289
  else
277
- @histogram = registry.histogram(element['name'].to_sym, element['desc'])
290
+ @histogram = registry.histogram(element['name'].to_sym, docstring: element['desc'], labels: @base_labels.keys)
278
291
  end
279
292
  rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
280
293
  @histogram = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :histogram, element['desc'])
281
294
  end
282
295
  end
283
296
 
284
- def instrument(record, expander, placeholders)
297
+ def instrument(record, expander)
285
298
  if @key.is_a?(String)
286
299
  value = record[@key]
287
300
  else
288
301
  value = @key.call(record)
289
302
  end
290
303
  if value
291
- @histogram.observe(labels(record, expander, placeholders), value)
304
+ @histogram.observe(value, labels: labels(record, expander))
292
305
  end
293
306
  end
294
307
  end