opentelemetry-metrics-sdk 0.12.0 → 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 +5 -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 +34 -7
  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 -25
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b9f9ff1d02193fd4d346d99c5f581e1e83eba6018315705e1e31355e1fe75fc
4
- data.tar.gz: cb95d4e5aba02bb9d257d5a0298cce4c014a2cd7297450e6c4d25437a59c6b0d
3
+ metadata.gz: ab86f13e4c2f144a3c7e0016427f84cd3b441e73ea7a1763b87ce0d65cdfe09a
4
+ data.tar.gz: 0a6108e36f68f835015d2788dfda936fe32dd9dece37c89f9544cc2954598a89
5
5
  SHA512:
6
- metadata.gz: be225d34da7104c077cad530fcec9360feb31c9d930e393ac5c3e1509809b9511c2d39cbd5ebb35ff2e2d16569935c7b41d974fac8acb5b2bac1acbe2e12d8c7
7
- data.tar.gz: 9590ecc610905a827b266e50f014ff653abc72bc7dee204351259218dda3144ce8d5519c9372d12ee876f82a484cf34aa71510ab096ddeacb5b44322c7896511
6
+ metadata.gz: 63c87d0216721b418f168d92e419006c3b552a0eb1dd2de9b804f773a7928c14505eac77691ef92782ab69a7d457c916cb162a89098eee9fa382f8ac3e27526d
7
+ data.tar.gz: fe4b4ad19c93bdb896d66d54c65a3500eb7e1237af6d1c992b64129ad050fe93351bb7a6e1812035db123fc81795ccb29be479c48593c0f2f45402cbac60d490
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ### v0.12.0 / 2026-02-11
4
9
 
5
10
  * BREAKING CHANGE: Fix the issue of mixed scale with multiple attributes
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,11 @@ 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
+
57
+ @mapping = new_mapping(@scale)
58
+
47
59
  # Previous state for cumulative aggregation
48
60
  @previous_positive = {} # nil
49
61
  @previous_negative = {} # nil
@@ -67,6 +79,8 @@ module OpenTelemetry
67
79
  hdps = data_points.values.map! do |hdp|
68
80
  hdp.start_time_unix_nano = start_time
69
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)
70
84
  hdp
71
85
  end
72
86
  data_points.clear
@@ -146,6 +160,7 @@ module OpenTelemetry
146
160
  @previous_scale[attributes] = min_scale
147
161
 
148
162
  # Create merged data point
163
+ reservoir = @exemplar_reservoir_storage[attributes]
149
164
  merged_hdp = ExponentialHistogramDataPoint.new(
150
165
  attributes,
151
166
  start_time,
@@ -157,7 +172,7 @@ module OpenTelemetry
157
172
  @previous_positive[attributes].dup,
158
173
  @previous_negative[attributes].dup,
159
174
  0, # flags
160
- nil, # exemplars
175
+ reservoir&.collect(attributes: attributes, aggregation_temporality: @aggregation_temporality), # exemplars
161
176
  @previous_min[attributes],
162
177
  @previous_max[attributes],
163
178
  @zero_threshold
@@ -172,6 +187,7 @@ module OpenTelemetry
172
187
  # so return last merged data points if exists
173
188
  if data_points.empty? && !@previous_positive.empty?
174
189
  @previous_positive.each_key do |attributes|
190
+ reservoir = @exemplar_reservoir_storage[attributes]
175
191
  merged_hdp = ExponentialHistogramDataPoint.new(
176
192
  attributes,
177
193
  start_time,
@@ -183,7 +199,7 @@ module OpenTelemetry
183
199
  @previous_positive[attributes].dup,
184
200
  @previous_negative[attributes].dup,
185
201
  0, # flags
186
- nil, # exemplars
202
+ reservoir&.collect(attributes: attributes, aggregation_temporality: @aggregation_temporality), # exemplars
187
203
  @previous_min[attributes],
188
204
  @previous_max[attributes],
189
205
  @zero_threshold
@@ -206,7 +222,7 @@ module OpenTelemetry
206
222
 
207
223
  # this is aggregate in python; there is no merge in aggregate; but rescale happened
208
224
  # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
209
- def update(amount, attributes, data_points)
225
+ def update(amount, attributes, data_points, exemplar_offer: false)
210
226
  # fetch or initialize the ExponentialHistogramDataPoint
211
227
  hdp = data_points.fetch(attributes) do
212
228
  if @record_min_max
@@ -233,6 +249,20 @@ module OpenTelemetry
233
249
  )
234
250
  end
235
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
+
236
266
  # Start to populate the data point (esp. the buckets)
237
267
  if @record_min_max
238
268
  hdp.max = amount if amount > hdp.max
@@ -336,9 +366,6 @@ module OpenTelemetry
336
366
  end
337
367
 
338
368
  def get_scale_change(low, high)
339
- # puts "get_scale_change: low: #{low}, high: #{high}, @size: #{@size}"
340
- # python code also produce 18 with 0,1048575, the high is little bit off
341
- # just checked, the mapping is also ok, produce the 1048575
342
369
  change = 0
343
370
  while high - low >= @size
344
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,
@@ -11,9 +11,20 @@ module OpenTelemetry
11
11
  # Contains the implementation of the Sum aggregation
12
12
  # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#sum-aggregation
13
13
  class Sum
14
- def initialize(aggregation_temporality: ENV.fetch('OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE', :cumulative), monotonic: false, instrument_kind: nil)
14
+ attr_reader :exemplar_reservoir
15
+
16
+ # if no reservior pass from instrument, then use this empty reservior to avoid no method found error
17
+ DEFAULT_RESERVOIR = Metrics::Exemplar::SimpleFixedSizeExemplarReservoir.new
18
+ private_constant :DEFAULT_RESERVOIR
19
+
20
+ def initialize(aggregation_temporality: ENV.fetch('OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE', :cumulative),
21
+ monotonic: false,
22
+ instrument_kind: nil,
23
+ exemplar_reservoir: nil)
15
24
  @aggregation_temporality = AggregationTemporality.determine_temporality(aggregation_temporality: aggregation_temporality, instrument_kind: instrument_kind, default: :cumulative)
16
25
  @monotonic = monotonic
26
+ @exemplar_reservoir = exemplar_reservoir || DEFAULT_RESERVOIR
27
+ @exemplar_reservoir_storage = {}
17
28
  end
18
29
 
19
30
  def collect(start_time, end_time, data_points)
@@ -22,6 +33,8 @@ module OpenTelemetry
22
33
  ndps = data_points.values.map! do |ndp|
23
34
  ndp.start_time_unix_nano = start_time
24
35
  ndp.time_unix_nano = end_time
36
+ reservoir = @exemplar_reservoir_storage[ndp.attributes]
37
+ ndp.exemplars = reservoir&.collect(attributes: ndp.attributes, aggregation_temporality: @aggregation_temporality)
25
38
  ndp
26
39
  end
27
40
  data_points.clear
@@ -31,6 +44,8 @@ module OpenTelemetry
31
44
  data_points.values.map! do |ndp|
32
45
  ndp.start_time_unix_nano ||= start_time # Start time of a data point is from the first observation.
33
46
  ndp.time_unix_nano = end_time
47
+ reservoir = @exemplar_reservoir_storage[ndp.attributes]
48
+ ndp.exemplars = reservoir&.collect(attributes: ndp.attributes, aggregation_temporality: @aggregation_temporality)
34
49
  ndp.dup
35
50
  end
36
51
  end
@@ -40,7 +55,7 @@ module OpenTelemetry
40
55
  @monotonic
41
56
  end
42
57
 
43
- def update(increment, attributes, data_points)
58
+ def update(increment, attributes, data_points, exemplar_offer: false)
44
59
  return if @monotonic && increment < 0
45
60
 
46
61
  ndp = data_points[attributes] || data_points[attributes] = NumberDataPoint.new(
@@ -51,6 +66,20 @@ module OpenTelemetry
51
66
  nil
52
67
  )
53
68
 
69
+ reservoir = @exemplar_reservoir_storage[attributes]
70
+ unless reservoir
71
+ reservoir = @exemplar_reservoir.dup
72
+ reservoir.reset
73
+ @exemplar_reservoir_storage[attributes] = reservoir
74
+ end
75
+
76
+ if exemplar_offer
77
+ reservoir.offer(value: increment,
78
+ timestamp: OpenTelemetry::Common::Utilities.time_in_nanoseconds,
79
+ attributes: attributes,
80
+ context: OpenTelemetry::Context.current)
81
+ end
82
+
54
83
  ndp.value += increment
55
84
  nil
56
85
  end