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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +237 -28
- data/lib/opentelemetry/sdk/metrics/aggregation/drop.rb +10 -2
- data/lib/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram.rb +25 -2
- data/lib/opentelemetry/sdk/metrics/aggregation/exponential_bucket_histogram.rb +61 -17
- data/lib/opentelemetry/sdk/metrics/aggregation/last_value.rb +28 -1
- data/lib/opentelemetry/sdk/metrics/aggregation/sum.rb +31 -2
- data/lib/opentelemetry/sdk/metrics/exemplar/aligned_histogram_bucket_exemplar_reservoir.rb +72 -0
- data/lib/opentelemetry/sdk/metrics/exemplar/always_off_exemplar_filter.rb +21 -0
- data/lib/opentelemetry/sdk/metrics/exemplar/always_on_exemplar_filter.rb +20 -0
- data/lib/opentelemetry/sdk/metrics/exemplar/exemplar.rb +23 -0
- data/lib/opentelemetry/sdk/metrics/exemplar/exemplar_bucket.rb +75 -0
- data/lib/opentelemetry/sdk/metrics/exemplar/exemplar_filter.rb +25 -0
- data/lib/opentelemetry/sdk/metrics/exemplar/exemplar_reservoir.rb +39 -0
- data/lib/opentelemetry/sdk/metrics/exemplar/noop_exemplar_reservoir.rb +22 -0
- data/lib/opentelemetry/sdk/metrics/exemplar/simple_fixed_size_exemplar_reservoir.rb +59 -0
- data/lib/opentelemetry/sdk/metrics/exemplar/trace_based_exemplar_filter.rb +27 -0
- data/lib/opentelemetry/sdk/metrics/exemplar.rb +32 -0
- data/lib/opentelemetry/sdk/metrics/instrument/asynchronous_instrument.rb +18 -2
- data/lib/opentelemetry/sdk/metrics/instrument/counter.rb +1 -1
- data/lib/opentelemetry/sdk/metrics/instrument/gauge.rb +1 -1
- data/lib/opentelemetry/sdk/metrics/instrument/histogram.rb +3 -1
- data/lib/opentelemetry/sdk/metrics/instrument/observable_counter.rb +1 -1
- data/lib/opentelemetry/sdk/metrics/instrument/observable_gauge.rb +1 -1
- data/lib/opentelemetry/sdk/metrics/instrument/observable_up_down_counter.rb +1 -1
- data/lib/opentelemetry/sdk/metrics/instrument/synchronous_instrument.rb +6 -2
- data/lib/opentelemetry/sdk/metrics/instrument/up_down_counter.rb +1 -1
- data/lib/opentelemetry/sdk/metrics/meter.rb +8 -8
- data/lib/opentelemetry/sdk/metrics/meter_provider.rb +33 -1
- data/lib/opentelemetry/sdk/metrics/state/asynchronous_metric_stream.rb +12 -4
- data/lib/opentelemetry/sdk/metrics/state/metric_stream.rb +21 -4
- data/lib/opentelemetry/sdk/metrics/version.rb +1 -1
- data/lib/opentelemetry/sdk/metrics.rb +1 -0
- metadata +19 -39
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ab86f13e4c2f144a3c7e0016427f84cd3b441e73ea7a1763b87ce0d65cdfe09a
|
|
4
|
+
data.tar.gz: 0a6108e36f68f835015d2788dfda936fe32dd9dece37c89f9544cc2954598a89
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
*
|
|
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
|
-
#
|
|
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
|
-
|
|
63
|
-
|
|
123
|
+
console_exporter = OpenTelemetry::SDK::Metrics::Export::ConsoleMetricPullExporter.new
|
|
124
|
+
OpenTelemetry.meter_provider.add_metric_reader(console_exporter)
|
|
64
125
|
|
|
65
|
-
|
|
66
|
-
OpenTelemetry.meter_provider.add_metric_reader(console_metric_exporter)
|
|
126
|
+
meter = OpenTelemetry.meter_provider.meter('my_app')
|
|
67
127
|
|
|
68
|
-
#
|
|
69
|
-
|
|
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
|
-
#
|
|
72
|
-
|
|
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
|
-
#
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
219
|
-
ExponentialHistogram::Buckets.new,
|
|
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
|
|
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
|
-
|
|
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 =
|
|
272
|
-
|
|
273
|
-
|
|
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 =
|
|
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,
|