opentelemetry-metrics-sdk 0.11.2 → 0.13.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +237 -28
  4. data/lib/opentelemetry/sdk/metrics/aggregation/drop.rb +10 -2
  5. data/lib/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram.rb +25 -2
  6. data/lib/opentelemetry/sdk/metrics/aggregation/exponential_bucket_histogram.rb +61 -17
  7. data/lib/opentelemetry/sdk/metrics/aggregation/last_value.rb +28 -1
  8. data/lib/opentelemetry/sdk/metrics/aggregation/sum.rb +31 -2
  9. data/lib/opentelemetry/sdk/metrics/exemplar/aligned_histogram_bucket_exemplar_reservoir.rb +72 -0
  10. data/lib/opentelemetry/sdk/metrics/exemplar/always_off_exemplar_filter.rb +21 -0
  11. data/lib/opentelemetry/sdk/metrics/exemplar/always_on_exemplar_filter.rb +20 -0
  12. data/lib/opentelemetry/sdk/metrics/exemplar/exemplar.rb +23 -0
  13. data/lib/opentelemetry/sdk/metrics/exemplar/exemplar_bucket.rb +75 -0
  14. data/lib/opentelemetry/sdk/metrics/exemplar/exemplar_filter.rb +25 -0
  15. data/lib/opentelemetry/sdk/metrics/exemplar/exemplar_reservoir.rb +39 -0
  16. data/lib/opentelemetry/sdk/metrics/exemplar/noop_exemplar_reservoir.rb +22 -0
  17. data/lib/opentelemetry/sdk/metrics/exemplar/simple_fixed_size_exemplar_reservoir.rb +59 -0
  18. data/lib/opentelemetry/sdk/metrics/exemplar/trace_based_exemplar_filter.rb +27 -0
  19. data/lib/opentelemetry/sdk/metrics/exemplar.rb +32 -0
  20. data/lib/opentelemetry/sdk/metrics/instrument/asynchronous_instrument.rb +18 -2
  21. data/lib/opentelemetry/sdk/metrics/instrument/counter.rb +1 -1
  22. data/lib/opentelemetry/sdk/metrics/instrument/gauge.rb +1 -1
  23. data/lib/opentelemetry/sdk/metrics/instrument/histogram.rb +3 -1
  24. data/lib/opentelemetry/sdk/metrics/instrument/observable_counter.rb +1 -1
  25. data/lib/opentelemetry/sdk/metrics/instrument/observable_gauge.rb +1 -1
  26. data/lib/opentelemetry/sdk/metrics/instrument/observable_up_down_counter.rb +1 -1
  27. data/lib/opentelemetry/sdk/metrics/instrument/synchronous_instrument.rb +6 -2
  28. data/lib/opentelemetry/sdk/metrics/instrument/up_down_counter.rb +1 -1
  29. data/lib/opentelemetry/sdk/metrics/meter.rb +8 -8
  30. data/lib/opentelemetry/sdk/metrics/meter_provider.rb +33 -1
  31. data/lib/opentelemetry/sdk/metrics/state/asynchronous_metric_stream.rb +12 -4
  32. data/lib/opentelemetry/sdk/metrics/state/metric_stream.rb +21 -4
  33. data/lib/opentelemetry/sdk/metrics/version.rb +1 -1
  34. data/lib/opentelemetry/sdk/metrics.rb +1 -0
  35. metadata +19 -39
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f62ae36ad71651af824a64c15bd3ffff2e32c08cd161655a013955736f97a95
4
- data.tar.gz: 7c1a32329e65f2a96e5ee4c6715067cb488e2b5c88e2642d3668d2eddfbe9ce3
3
+ metadata.gz: ab86f13e4c2f144a3c7e0016427f84cd3b441e73ea7a1763b87ce0d65cdfe09a
4
+ data.tar.gz: 0a6108e36f68f835015d2788dfda936fe32dd9dece37c89f9544cc2954598a89
5
5
  SHA512:
6
- metadata.gz: 8286d7d68a62f7629e05fbfc7718743b583268a24882f390e60fdcdf584a27dc7aef7d487d209caafd179966c713969334c06fb2ad230f3865686627b62a88ef
7
- data.tar.gz: e43fca566da12585f81251e896175a4b627af4403aa9d146d733ebce117cadf08836d1687d01acfe024c7f9ef06a395f3122ba080201249677eb9b154b9bedd9
6
+ metadata.gz: 63c87d0216721b418f168d92e419006c3b552a0eb1dd2de9b804f773a7928c14505eac77691ef92782ab69a7d457c916cb162a89098eee9fa382f8ac3e27526d
7
+ data.tar.gz: fe4b4ad19c93bdb896d66d54c65a3500eb7e1237af6d1c992b64129ad050fe93351bb7a6e1812035db123fc81795ccb29be479c48593c0f2f45402cbac60d490
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Release History: opentelemetry-metrics-sdk
2
2
 
3
+ ### v0.13.0 / 2026-04-07
4
+
5
+ * ADDED: Min Ruby Version 3.3 (#2070)
6
+ * ADDED: Add basic support for metrics exemplar (#1609)
7
+
8
+ ### v0.12.0 / 2026-02-11
9
+
10
+ * BREAKING CHANGE: Fix the issue of mixed scale with multiple attributes
11
+
12
+ * FIXED: Fix the issue of mixed scale with multiple attributes
13
+
3
14
  ### v0.11.2 / 2025-12-02
4
15
 
5
16
  * FIXED: Add merge logic for exponential histogram when the temporality cumulative
data/README.md CHANGED
@@ -12,28 +12,36 @@ OpenTelemetry provides a single set of APIs, libraries, agents, and collector se
12
12
 
13
13
  Metrics is one of the core signals in OpenTelemetry. This package allows you to emit OpenTelemetry metrics using Ruby. It leverages an alpha implementation of the OpenTelemetry Metrics API. At the current stage, things may break and APIs may change. Use this tool with caution.
14
14
 
15
- This gem does not have a full implementation of the Metrics SDK specification. The work is in progress.
15
+ This gem does not yet have a full implementation of the Metrics SDK specification. Work is in progress.
16
16
 
17
17
  At this time, you should be able to:
18
18
 
19
- * Create synchronous:
20
- * counters
21
- * up down counters
22
- * histograms
23
- * observable counters
24
- * observable gauges
25
- * observable up down counters
26
- * Export using a pull exporter
27
- * Use delta aggregation temporality
28
- * Periodic Exporting Metric Reader
19
+ * Create all synchronous instruments:
20
+ * `Counter`
21
+ * `UpDownCounter`
22
+ * `Histogram`
23
+ * `Gauge`
24
+ * Create all asynchronous (observable) instruments:
25
+ * `ObservableCounter`
26
+ * `ObservableGauge`
27
+ * `ObservableUpDownCounter`
28
+ * Use all aggregation types:
29
+ * `ExplicitBucketHistogram` (default for histograms)
30
+ * `ExponentialBucketHistogram`
31
+ * `Sum` (default for counters and up-down counters)
32
+ * `LastValue` (default for gauges)
33
+ * `Drop`
34
+ * Configure aggregation temporality: delta, cumulative, or low-memory
35
+ * Customize metric collection with Views (filter by name, type, unit, aggregation, attribute keys)
36
+ * Export metrics using pull-based exporters:
37
+ * `ConsoleMetricPullExporter`
38
+ * `InMemoryMetricPullExporter` (for testing)
39
+ * Export metrics on a schedule using `PeriodicMetricReader` with any compatible push exporter (e.g. OTLP via `opentelemetry-exporter-otlp-metrics`)
29
40
 
30
41
  We do not yet have support for:
31
42
 
32
- * Asynchronous instruments
33
- * Cumulative aggregation temporality
34
- * Metrics Views
35
43
  * Metrics Exemplars
36
- * Push metric exporting
44
+ * `schema_url` in view configuration
37
45
 
38
46
  These lists are incomplete and are intended to give a broad description of what's available.
39
47
 
@@ -56,31 +64,232 @@ Then, configure the SDK according to your desired handling of telemetry data, an
56
64
  require 'opentelemetry/sdk'
57
65
  require 'opentelemetry-metrics-sdk'
58
66
 
59
- # Configure the sdk with default export and context propagation formats.
67
+ # Disable automatic exporter configuration so we can set one manually.
68
+ ENV['OTEL_METRICS_EXPORTER'] = 'none'
69
+
70
+ OpenTelemetry::SDK.configure
71
+
72
+ # Create an exporter and register it with the meter provider.
73
+ console_exporter = OpenTelemetry::SDK::Metrics::Export::ConsoleMetricPullExporter.new
74
+ OpenTelemetry.meter_provider.add_metric_reader(console_exporter)
75
+
76
+ # Create a meter and instrument.
77
+ meter = OpenTelemetry.meter_provider.meter('my_app')
78
+ histogram = meter.create_histogram('http.request.duration', unit: 'ms', description: 'HTTP request duration')
79
+
80
+ # Record a measurement.
81
+ histogram.record(200, attributes: { 'http.method' => 'GET', 'http.status_code' => '200' })
82
+
83
+ # Flush metrics to the exporter.
84
+ OpenTelemetry.meter_provider.metric_readers.each(&:pull)
85
+
86
+ OpenTelemetry.meter_provider.shutdown
87
+ ```
88
+
89
+ ### All synchronous instruments
90
+
91
+ ```ruby
92
+ meter = OpenTelemetry.meter_provider.meter('my_app')
93
+
94
+ # Counter — monotonically increasing value
95
+ counter = meter.create_counter('requests.total', unit: '1', description: 'Total requests')
96
+ counter.add(1, attributes: { 'service' => 'web' })
97
+
98
+ # UpDownCounter — value that can increase or decrease
99
+ queue_depth = meter.create_up_down_counter('queue.depth', unit: '1', description: 'Items in queue')
100
+ queue_depth.add(5)
101
+ queue_depth.add(-3)
102
+
103
+ # Histogram — distribution of measurements
104
+ duration = meter.create_histogram('db.query.duration', unit: 'ms', description: 'Database query duration')
105
+ duration.record(42, attributes: { 'db.operation' => 'SELECT' })
106
+
107
+ # Gauge — current value at observation time
108
+ temperature = meter.create_gauge('system.temperature', unit: 'cel', description: 'Current temperature')
109
+ temperature.record(23.5, attributes: { 'sensor' => 'cpu' })
110
+ ```
111
+
112
+ ### Asynchronous (observable) instruments
113
+
114
+ Asynchronous instruments collect measurements via a callback that is invoked when the metric reader collects data.
115
+
116
+ ```ruby
117
+ require 'opentelemetry/sdk'
118
+ require 'opentelemetry-metrics-sdk'
119
+
120
+ ENV['OTEL_METRICS_EXPORTER'] = 'none'
60
121
  OpenTelemetry::SDK.configure
61
122
 
62
- # Create an exporter. This example exports metrics to the console.
63
- console_metric_exporter = OpenTelemetry::SDK::Metrics::Export::ConsoleMetricPullExporter.new
123
+ console_exporter = OpenTelemetry::SDK::Metrics::Export::ConsoleMetricPullExporter.new
124
+ OpenTelemetry.meter_provider.add_metric_reader(console_exporter)
64
125
 
65
- # Add the exporter to the meter provider as a new metric reader.
66
- OpenTelemetry.meter_provider.add_metric_reader(console_metric_exporter)
126
+ meter = OpenTelemetry.meter_provider.meter('my_app')
67
127
 
68
- # Create a meter to generate instruments.
69
- meter = OpenTelemetry.meter_provider.meter("SAMPLE_METER_NAME")
128
+ # ObservableCounter monotonically increasing, measured on demand
129
+ cpu_callback = proc { `ps -p #{Process.pid} -o %cpu=`.strip.to_f }
130
+ cpu_counter = meter.create_observable_counter('process.cpu.usage', callback: cpu_callback, unit: 'ms')
70
131
 
71
- # Create an instrument.
72
- histogram = meter.create_histogram('histogram', unit: 'smidgen', description: 'desscription')
132
+ # ObservableGauge current value, measured on demand
133
+ mem_callback = proc { `ps -p #{Process.pid} -o %mem=`.strip.to_f }
134
+ mem_gauge = meter.create_observable_gauge('process.memory.usage', callback: mem_callback, unit: 'percent')
73
135
 
74
- # Record a metric.
75
- histogram.record(123, attributes: {'foo' => 'bar'})
136
+ # ObservableUpDownCounter can increase or decrease, measured on demand
137
+ queue_callback = proc { JobQueue.current_depth }
138
+ queue_counter = meter.create_observable_up_down_counter('jobs.queue.depth', callback: queue_callback, unit: '1')
139
+
140
+ # Trigger callbacks and export
141
+ cpu_counter.observe
142
+ mem_gauge.observe
143
+ queue_counter.observe
76
144
 
77
- # Send the recorded metrics to the metric readers.
78
145
  OpenTelemetry.meter_provider.metric_readers.each(&:pull)
146
+ OpenTelemetry.meter_provider.shutdown
147
+ ```
148
+
149
+ ### Views
150
+
151
+ Views let you customize how metrics are collected and exported — changing the aggregation, filtering attribute keys, or dropping instruments entirely.
79
152
 
80
- # Shut down the meter provider.
153
+ #### Change aggregation for a specific instrument
154
+
155
+ ```ruby
156
+ require 'opentelemetry/sdk'
157
+ require 'opentelemetry-metrics-sdk'
158
+
159
+ ENV['OTEL_METRICS_EXPORTER'] = 'none'
160
+ OpenTelemetry::SDK.configure
161
+
162
+ console_exporter = OpenTelemetry::SDK::Metrics::Export::ConsoleMetricPullExporter.new
163
+ OpenTelemetry.meter_provider.add_metric_reader(console_exporter)
164
+
165
+ # Use exponential histogram aggregation for any histogram whose name contains "exponential".
166
+ # The view name supports * (match any characters) and ? (match one character) wildcards.
167
+ OpenTelemetry.meter_provider.add_view(
168
+ '*exponential*',
169
+ aggregation: OpenTelemetry::SDK::Metrics::Aggregation::ExponentialBucketHistogram.new(
170
+ aggregation_temporality: :cumulative,
171
+ max_scale: 20
172
+ ),
173
+ type: :histogram,
174
+ unit: 'ms'
175
+ )
176
+
177
+ meter = OpenTelemetry.meter_provider.meter('my_app')
178
+ hist = meter.create_histogram('http.exponential.latency', unit: 'ms', description: 'Latency distribution')
179
+ (1..10).each { |i| hist.record(i ** 2, attributes: { 'env' => 'prod' }) }
180
+
181
+ OpenTelemetry.meter_provider.metric_readers.each(&:pull)
81
182
  OpenTelemetry.meter_provider.shutdown
82
183
  ```
83
184
 
185
+ #### Drop an instrument
186
+
187
+ ```ruby
188
+ # Drop all metrics from a specific meter — useful for suppressing noisy or low-value instrumentation.
189
+ OpenTelemetry.meter_provider.add_view(
190
+ '*',
191
+ aggregation: OpenTelemetry::SDK::Metrics::Aggregation::Drop.new,
192
+ meter_name: 'noisy_library'
193
+ )
194
+ ```
195
+
196
+ #### Restrict which attribute keys are retained
197
+
198
+ ```ruby
199
+ # Only keep the 'http.method' and 'http.status_code' attributes; all others are dropped.
200
+ OpenTelemetry.meter_provider.add_view(
201
+ 'http.request.duration',
202
+ attribute_keys: { 'http.method' => nil, 'http.status_code' => nil }
203
+ )
204
+ ```
205
+
206
+ #### Full view options reference
207
+
208
+ ```ruby
209
+ OpenTelemetry.meter_provider.add_view(
210
+ 'instrument_name_pattern', # supports * and ? wildcards; matches against instrument name
211
+ aggregation: OpenTelemetry::SDK::Metrics::Aggregation::ExplicitBucketHistogram.new,
212
+ type: :histogram, # instrument kind: :counter, :up_down_counter, :histogram,
213
+ # :gauge, :observable_counter, :observable_gauge,
214
+ # :observable_up_down_counter
215
+ unit: 'ms', # matches instruments with this unit
216
+ meter_name: 'my_meter', # matches instruments from this meter
217
+ meter_version: '1.0', # matches instruments from this meter version
218
+ attribute_keys: { 'env' => nil, 'region' => nil } # allowlist of attribute keys to retain
219
+ )
220
+ ```
221
+
222
+ ### Aggregation temporality
223
+
224
+ Aggregation temporality controls whether exported values represent measurements since the last export (delta) or since the process started (cumulative). Configure it globally via the environment variable or per-aggregation:
225
+
226
+ ```ruby
227
+ # Via environment variable (applies to all OTLP exports):
228
+ # OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulative (or delta, or lowmemory)
229
+
230
+ # Per-aggregation:
231
+ sum_agg = OpenTelemetry::SDK::Metrics::Aggregation::Sum.new(aggregation_temporality: :delta)
232
+ hist_agg = OpenTelemetry::SDK::Metrics::Aggregation::ExplicitBucketHistogram.new(
233
+ aggregation_temporality: :cumulative,
234
+ boundaries: [0, 10, 50, 100, 500, 1000]
235
+ )
236
+
237
+ OpenTelemetry.meter_provider.add_view('my_counter', aggregation: sum_agg, type: :counter)
238
+ OpenTelemetry.meter_provider.add_view('my_histogram', aggregation: hist_agg, type: :histogram)
239
+ ```
240
+
241
+ | Temporality preference | Counter | Observable Counter | Histogram | UpDownCounter | Observable UpDownCounter |
242
+ | --- | --- | --- | --- | --- | --- |
243
+ | `cumulative` | Cumulative | Cumulative | Cumulative | Cumulative | Cumulative |
244
+ | `delta` | Delta | Delta | Delta | Cumulative | Cumulative |
245
+ | `lowmemory` | Delta | Cumulative | Delta | Cumulative | Cumulative |
246
+
247
+ ### Periodic exporting with PeriodicMetricReader
248
+
249
+ Use `PeriodicMetricReader` to wrap any push exporter (such as the OTLP exporter) and automatically export on a schedule:
250
+
251
+ ```ruby
252
+ require 'opentelemetry/sdk'
253
+ require 'opentelemetry-metrics-sdk'
254
+ require 'opentelemetry-exporter-otlp-metrics'
255
+
256
+ ENV['OTEL_METRICS_EXPORTER'] = 'none'
257
+ OpenTelemetry::SDK.configure
258
+
259
+ otlp_exporter = OpenTelemetry::Exporter::OTLP::Metrics::MetricsExporter.new
260
+ periodic_reader = OpenTelemetry::SDK::Metrics::Export::PeriodicMetricReader.new(
261
+ export_interval_millis: 5_000, # default: OTEL_METRIC_EXPORT_INTERVAL (60_000 ms)
262
+ export_timeout_millis: 1_000, # default: OTEL_METRIC_EXPORT_TIMEOUT (30_000 ms)
263
+ exporter: otlp_exporter
264
+ )
265
+ OpenTelemetry.meter_provider.add_metric_reader(periodic_reader)
266
+
267
+ meter = OpenTelemetry.meter_provider.meter('my_app')
268
+ counter = meter.create_counter('requests.total', unit: '1')
269
+ counter.add(1, attributes: { 'service' => 'web' })
270
+
271
+ OpenTelemetry.meter_provider.shutdown
272
+ ```
273
+
274
+ ### Using InMemoryMetricPullExporter for testing
275
+
276
+ ```ruby
277
+ require 'opentelemetry-metrics-sdk'
278
+
279
+ exporter = OpenTelemetry::SDK::Metrics::Export::InMemoryMetricPullExporter.new
280
+ OpenTelemetry.meter_provider.add_metric_reader(exporter)
281
+
282
+ meter = OpenTelemetry.meter_provider.meter('test_meter')
283
+ counter = meter.create_counter('test.counter', unit: '1')
284
+ counter.add(5, attributes: { 'env' => 'test' })
285
+
286
+ exporter.pull
287
+ snapshots = exporter.metric_snapshots # Array of MetricData structs
288
+ # => [#<struct name="test.counter", data_points=[...]>]
289
+
290
+ exporter.reset # clear accumulated snapshots between test cases
291
+ ```
292
+
84
293
  For additional examples, see the [examples on github][examples-github].
85
294
 
86
295
  ## How can I get involved?
@@ -10,17 +10,25 @@ module OpenTelemetry
10
10
  module Aggregation
11
11
  # Contains the implementation of the Drop aggregation
12
12
  class Drop
13
+ # Use noop reservoir since this is a no-op aggregation
14
+ DEFAULT_RESERVOIR = Metrics::Exemplar::NoopExemplarReservoir.new
15
+ private_constant :DEFAULT_RESERVOIR
16
+
17
+ def initialize(exemplar_reservoir: nil)
18
+ @exemplar_reservoir = DEFAULT_RESERVOIR
19
+ end
20
+
13
21
  def collect(start_time, end_time, data_points)
14
22
  data_points.values.map!(&:dup)
15
23
  end
16
24
 
17
- def update(increment, attributes, data_points)
25
+ def update(increment, attributes, data_points, exemplar_offer: false)
18
26
  data_points[attributes] = NumberDataPoint.new(
19
27
  {},
20
28
  0,
21
29
  0,
22
30
  0,
23
- 0
31
+ []
24
32
  )
25
33
  nil
26
34
  end
@@ -11,6 +11,8 @@ module OpenTelemetry
11
11
  # Contains the implementation of the ExplicitBucketHistogram aggregation
12
12
  # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#explicit-bucket-histogram-aggregation
13
13
  class ExplicitBucketHistogram
14
+ attr_reader :exemplar_reservoir
15
+
14
16
  DEFAULT_BOUNDARIES = [0, 5, 10, 25, 50, 75, 100, 250, 500, 1000].freeze
15
17
  private_constant :DEFAULT_BOUNDARIES
16
18
 
@@ -21,11 +23,14 @@ module OpenTelemetry
21
23
  def initialize(
22
24
  aggregation_temporality: ENV.fetch('OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE', :cumulative),
23
25
  boundaries: DEFAULT_BOUNDARIES,
24
- record_min_max: true
26
+ record_min_max: true,
27
+ exemplar_reservoir: nil
25
28
  )
26
29
  @aggregation_temporality = AggregationTemporality.determine_temporality(aggregation_temporality: aggregation_temporality, default: :cumulative)
27
30
  @boundaries = boundaries && !boundaries.empty? ? boundaries.sort : nil
28
31
  @record_min_max = record_min_max
32
+ @exemplar_reservoir = exemplar_reservoir || Metrics::Exemplar::AlignedHistogramBucketExemplarReservoir.new(boundaries: @boundaries)
33
+ @exemplar_reservoir_storage = {}
29
34
  end
30
35
 
31
36
  def collect(start_time, end_time, data_points)
@@ -34,6 +39,8 @@ module OpenTelemetry
34
39
  hdps = data_points.values.map! do |hdp|
35
40
  hdp.start_time_unix_nano = start_time
36
41
  hdp.time_unix_nano = end_time
42
+ reservoir = @exemplar_reservoir_storage[hdp.attributes]
43
+ hdp.exemplars = reservoir&.collect(attributes: hdp.attributes, aggregation_temporality: @aggregation_temporality)
37
44
  hdp
38
45
  end
39
46
  data_points.clear
@@ -43,6 +50,8 @@ module OpenTelemetry
43
50
  data_points.values.map! do |hdp|
44
51
  hdp.start_time_unix_nano ||= start_time # Start time of a data point is from the first observation.
45
52
  hdp.time_unix_nano = end_time
53
+ reservoir = @exemplar_reservoir_storage[hdp.attributes]
54
+ hdp.exemplars = reservoir&.collect(attributes: hdp.attributes, aggregation_temporality: @aggregation_temporality)
46
55
  hdp = hdp.dup
47
56
  hdp.bucket_counts = hdp.bucket_counts.dup
48
57
  hdp
@@ -50,7 +59,7 @@ module OpenTelemetry
50
59
  end
51
60
  end
52
61
 
53
- def update(amount, attributes, data_points)
62
+ def update(amount, attributes, data_points, exemplar_offer: false)
54
63
  hdp = data_points.fetch(attributes) do
55
64
  if @record_min_max
56
65
  min = Float::INFINITY
@@ -71,6 +80,20 @@ module OpenTelemetry
71
80
  )
72
81
  end
73
82
 
83
+ reservoir = @exemplar_reservoir_storage[attributes]
84
+ unless reservoir
85
+ reservoir = @exemplar_reservoir.dup
86
+ reservoir.reset
87
+ @exemplar_reservoir_storage[attributes] = reservoir
88
+ end
89
+
90
+ if exemplar_offer
91
+ reservoir.offer(value: amount,
92
+ timestamp: OpenTelemetry::Common::Utilities.time_in_nanoseconds,
93
+ attributes: attributes,
94
+ context: OpenTelemetry::Context.current)
95
+ end
96
+
74
97
  if @record_min_max
75
98
  hdp.max = amount if amount > hdp.max
76
99
  hdp.min = amount if amount < hdp.min
@@ -25,13 +25,20 @@ module OpenTelemetry
25
25
  MIN_MAX_SIZE = 2
26
26
  MAX_MAX_SIZE = 16_384
27
27
 
28
+ attr_reader :exemplar_reservoir
29
+
30
+ # if no reservoir pass from instrument, then use this empty reservoir to avoid no method found error
31
+ DEFAULT_RESERVOIR = Metrics::Exemplar::SimpleFixedSizeExemplarReservoir.new
32
+ private_constant :DEFAULT_RESERVOIR
33
+
28
34
  # The default boundaries are calculated based on default max_size and max_scale values
29
35
  def initialize(
30
36
  aggregation_temporality: ENV.fetch('OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE', :delta),
31
37
  max_size: DEFAULT_SIZE,
32
38
  max_scale: DEFAULT_SCALE,
33
39
  record_min_max: true,
34
- zero_threshold: 0
40
+ zero_threshold: 0,
41
+ exemplar_reservoir: nil
35
42
  )
36
43
  @aggregation_temporality = AggregationTemporality.determine_temporality(aggregation_temporality: aggregation_temporality, default: :delta)
37
44
  @record_min_max = record_min_max
@@ -44,6 +51,9 @@ module OpenTelemetry
44
51
  @size = validate_size(max_size)
45
52
  @scale = validate_scale(max_scale)
46
53
 
54
+ @exemplar_reservoir = exemplar_reservoir || DEFAULT_RESERVOIR
55
+ @exemplar_reservoir_storage = {}
56
+
47
57
  @mapping = new_mapping(@scale)
48
58
 
49
59
  # Previous state for cumulative aggregation
@@ -55,6 +65,10 @@ module OpenTelemetry
55
65
  @previous_count = {} # 0
56
66
  @previous_zero_count = {} # 0
57
67
  @previous_scale = {} # nil
68
+
69
+ # Cache mappings per attribute set
70
+ @mappings = {}
71
+ @previous_mappings = {}
58
72
  end
59
73
 
60
74
  # when aggregation temporality is cumulative, merge and downscale will happen.
@@ -65,9 +79,12 @@ module OpenTelemetry
65
79
  hdps = data_points.values.map! do |hdp|
66
80
  hdp.start_time_unix_nano = start_time
67
81
  hdp.time_unix_nano = end_time
82
+ reservoir = @exemplar_reservoir_storage[hdp.attributes]
83
+ hdp.exemplars = reservoir&.collect(attributes: hdp.attributes, aggregation_temporality: @aggregation_temporality)
68
84
  hdp
69
85
  end
70
86
  data_points.clear
87
+ @mappings.clear
71
88
  hdps
72
89
  else
73
90
  # CUMULATIVE temporality - merge current data_points to previous data_points
@@ -143,6 +160,7 @@ module OpenTelemetry
143
160
  @previous_scale[attributes] = min_scale
144
161
 
145
162
  # Create merged data point
163
+ reservoir = @exemplar_reservoir_storage[attributes]
146
164
  merged_hdp = ExponentialHistogramDataPoint.new(
147
165
  attributes,
148
166
  start_time,
@@ -154,13 +172,14 @@ module OpenTelemetry
154
172
  @previous_positive[attributes].dup,
155
173
  @previous_negative[attributes].dup,
156
174
  0, # flags
157
- nil, # exemplars
175
+ reservoir&.collect(attributes: attributes, aggregation_temporality: @aggregation_temporality), # exemplars
158
176
  @previous_min[attributes],
159
177
  @previous_max[attributes],
160
178
  @zero_threshold
161
179
  )
162
180
 
163
181
  merged_data_points[attributes] = merged_hdp
182
+ @previous_mappings[attributes] = @mappings[attributes] if @mappings[attributes] # Preserve mapping for next collection
164
183
  end
165
184
  # rubocop:enable Metrics/BlockLength
166
185
 
@@ -168,6 +187,7 @@ module OpenTelemetry
168
187
  # so return last merged data points if exists
169
188
  if data_points.empty? && !@previous_positive.empty?
170
189
  @previous_positive.each_key do |attributes|
190
+ reservoir = @exemplar_reservoir_storage[attributes]
171
191
  merged_hdp = ExponentialHistogramDataPoint.new(
172
192
  attributes,
173
193
  start_time,
@@ -179,7 +199,7 @@ module OpenTelemetry
179
199
  @previous_positive[attributes].dup,
180
200
  @previous_negative[attributes].dup,
181
201
  0, # flags
182
- nil, # exemplars
202
+ reservoir&.collect(attributes: attributes, aggregation_temporality: @aggregation_temporality), # exemplars
183
203
  @previous_min[attributes],
184
204
  @previous_max[attributes],
185
205
  @zero_threshold
@@ -188,6 +208,10 @@ module OpenTelemetry
188
208
  end
189
209
  end
190
210
 
211
+ # Swap current with previous mappings for next cycle
212
+ @mappings = @previous_mappings
213
+ @previous_mappings = {}
214
+
191
215
  # clear data_points since the data is merged into previous_* already;
192
216
  # otherwise we will have duplicated data_points in the next collect
193
217
  data_points.clear
@@ -197,8 +221,8 @@ module OpenTelemetry
197
221
  # rubocop:enable Metrics/MethodLength
198
222
 
199
223
  # this is aggregate in python; there is no merge in aggregate; but rescale happened
200
- # rubocop:disable Metrics/MethodLength
201
- def update(amount, attributes, data_points)
224
+ # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
225
+ def update(amount, attributes, data_points, exemplar_offer: false)
202
226
  # fetch or initialize the ExponentialHistogramDataPoint
203
227
  hdp = data_points.fetch(attributes) do
204
228
  if @record_min_max
@@ -215,16 +239,30 @@ module OpenTelemetry
215
239
  0, # :sum
216
240
  @scale, # :scale
217
241
  @zero_count, # :zero_count
218
- ExponentialHistogram::Buckets.new, # :positive
219
- ExponentialHistogram::Buckets.new, # :negative
242
+ ExponentialHistogram::Buckets.new, # :positive
243
+ ExponentialHistogram::Buckets.new, # :negative
220
244
  0, # :flags
221
245
  nil, # :exemplars
222
246
  min, # :min
223
247
  max, # :max
224
- @zero_threshold # :zero_threshold)
248
+ @zero_threshold # :zero_threshold
225
249
  )
226
250
  end
227
251
 
252
+ reservoir = @exemplar_reservoir_storage[attributes]
253
+ unless reservoir
254
+ reservoir = @exemplar_reservoir.dup
255
+ reservoir.reset
256
+ @exemplar_reservoir_storage[attributes] = reservoir
257
+ end
258
+
259
+ if exemplar_offer
260
+ reservoir.offer(value: amount,
261
+ timestamp: OpenTelemetry::Common::Utilities.time_in_nanoseconds,
262
+ attributes: attributes,
263
+ context: OpenTelemetry::Context.current)
264
+ end
265
+
228
266
  # Start to populate the data point (esp. the buckets)
229
267
  if @record_min_max
230
268
  hdp.max = amount if amount > hdp.max
@@ -244,7 +282,15 @@ module OpenTelemetry
244
282
  buckets = amount.positive? ? hdp.positive : hdp.negative
245
283
  amount = -amount if amount.negative?
246
284
 
247
- bucket_index = @mapping.map_to_index(amount)
285
+ # Reset scale to max_scale if transitioning from all-zeros to first non-zero value
286
+ if buckets.counts == [0] && hdp.scale == 0 && hdp.count > hdp.zero_count
287
+ hdp.scale = @scale
288
+ @mappings.delete(attributes) # Clear any cached mapping
289
+ end
290
+
291
+ # Get or create mapping for this attribute set
292
+ mapping = @mappings[attributes] ||= new_mapping(hdp.scale)
293
+ bucket_index = mapping.map_to_index(amount)
248
294
 
249
295
  rescaling_needed = false
250
296
  low = high = 0
@@ -268,14 +314,15 @@ module OpenTelemetry
268
314
  if rescaling_needed
269
315
  scale_change = get_scale_change(low, high)
270
316
  downscale(scale_change, hdp.positive, hdp.negative)
271
- new_scale = @mapping.scale - scale_change
272
- @mapping = new_mapping(new_scale)
273
- bucket_index = @mapping.map_to_index(amount)
317
+ new_scale = mapping.scale - scale_change
318
+ mapping = new_mapping(new_scale)
319
+ @mappings[attributes] = mapping # Update cache
320
+ bucket_index = mapping.map_to_index(amount)
274
321
 
275
322
  OpenTelemetry.logger.debug "Rescaled with new scale #{new_scale} from #{low} and #{high}; bucket_index is updated to #{bucket_index}"
276
323
  end
277
324
 
278
- hdp.scale = @mapping.scale
325
+ hdp.scale = mapping.scale
279
326
 
280
327
  # adjust buckets based on the bucket_index
281
328
  if bucket_index < buckets.index_start
@@ -294,7 +341,7 @@ module OpenTelemetry
294
341
  buckets.increment_bucket(bucket_index)
295
342
  nil
296
343
  end
297
- # rubocop:enable Metrics/MethodLength
344
+ # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity
298
345
 
299
346
  def aggregation_temporality
300
347
  @aggregation_temporality.temporality
@@ -319,9 +366,6 @@ module OpenTelemetry
319
366
  end
320
367
 
321
368
  def get_scale_change(low, high)
322
- # puts "get_scale_change: low: #{low}, high: #{high}, @size: #{@size}"
323
- # python code also produce 18 with 0,1048575, the high is little bit off
324
- # just checked, the mapping is also ok, produce the 1048575
325
369
  change = 0
326
370
  while high - low >= @size
327
371
  high >>= 1
@@ -10,17 +10,44 @@ module OpenTelemetry
10
10
  module Aggregation
11
11
  # Contains the implementation of the LastValue aggregation
12
12
  class LastValue
13
+ attr_reader :exemplar_reservoir
14
+
15
+ # if no reservoir pass from instrument, then use this empty reservoir to avoid no method found error
16
+ DEFAULT_RESERVOIR = Metrics::Exemplar::SimpleFixedSizeExemplarReservoir.new
17
+ private_constant :DEFAULT_RESERVOIR
18
+
19
+ def initialize(exemplar_reservoir: nil)
20
+ @exemplar_reservoir = exemplar_reservoir || DEFAULT_RESERVOIR
21
+ @exemplar_reservoir_storage = {}
22
+ end
23
+
13
24
  def collect(start_time, end_time, data_points)
14
25
  ndps = data_points.values.map! do |ndp|
15
26
  ndp.start_time_unix_nano = start_time
16
27
  ndp.time_unix_nano = end_time
28
+ reservoir = @exemplar_reservoir_storage[ndp.attributes]
29
+ ndp.exemplars = reservoir&.collect(attributes: ndp.attributes, aggregation_temporality: :delta)
17
30
  ndp
18
31
  end
19
32
  data_points.clear
20
33
  ndps
21
34
  end
22
35
 
23
- def update(increment, attributes, data_points)
36
+ def update(increment, attributes, data_points, exemplar_offer: false)
37
+ reservoir = @exemplar_reservoir_storage[attributes]
38
+ unless reservoir
39
+ reservoir = @exemplar_reservoir.dup
40
+ reservoir.reset
41
+ @exemplar_reservoir_storage[attributes] = reservoir
42
+ end
43
+
44
+ if exemplar_offer
45
+ reservoir.offer(value: increment,
46
+ timestamp: OpenTelemetry::Common::Utilities.time_in_nanoseconds,
47
+ attributes: attributes,
48
+ context: OpenTelemetry::Context.current)
49
+ end
50
+
24
51
  data_points[attributes] = NumberDataPoint.new(
25
52
  attributes,
26
53
  nil,