opentelemetry-metrics-sdk 0.18.0 → 0.19.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30c3d77013ce3c1eae95194915c2bf428d9a34ad2afbd29560dcda6c21a1cbfe
4
- data.tar.gz: e265066d90971699173482f848b00259081eaa4b335bb52126148863afa152d8
3
+ metadata.gz: 3f876db9fe6efa57838509b6ffc7e763f08f752f195169266d47fb5e87d7fba6
4
+ data.tar.gz: 397d5c293f18de20edb1c939cd750f4149a9944e70cd9b4c1f411559a4488bdd
5
5
  SHA512:
6
- metadata.gz: 10b82812fae8f1f8727eb8f79532e370435878edc8996337caa9313b6a2af3b183487feb5f5c4fc61fec07d6cd75bbd05fee14eb7aa8331b8168949c075b1698
7
- data.tar.gz: d91bc46c7dd8609486a78186947f025390d9bc8e039e142f9e5d5536aaf1199ca6416a6fb28234e80433fded78b2f1a282d9aae888fd8daf779d600bf6c3d23d
6
+ metadata.gz: e541f4daaa1e6c0228e22d297f18239e743917b16dcd047b9dfd2a87954fa68b0cfb1ba1401ca1274d7183610088790d6e45d9d79325859919673f4c31312083
7
+ data.tar.gz: cf8568c155bd447989b1082b0dc956d2f75fc7cfa2b333580c5067b4cf72ec01effdef928081f945dad097c6ffe170f2ab2274444cf5ac9780393617ccb1b9cc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Release History: opentelemetry-metrics-sdk
2
2
 
3
+ ### v0.19.0 / 2026-09-29
4
+
5
+ * BREAKING CHANGE: Update api for spec compliance (#2309)
6
+ * ADDED: Update api for spec compliance (#2309)
7
+ * ADDED: Move otlp exporter uri builder to common (#2240)
8
+ * FIXED: Scope shared aggregation state to the owning stream (#2300) (#2314)
9
+ * FIXED: Size exponential histogram exemplars (#2392)
10
+ * FIXED: Accept arbitrary instrument metadata (#2395)
11
+ * FIXED: Prevent readers spanning providers (#2393)
12
+
3
13
  ### v0.18.0 / 2026-09-09
4
14
 
5
15
  * BREAKING CHANGE: Align with the spec for metrics sdk and metrics otlp exporter (#2301)
@@ -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 # rubocop:disable Metrics/ClassLength
14
+ include StreamScopedStorage
15
+
14
16
  OVERFLOW_ATTRIBUTE_SET = { 'otel.metric.overflow' => true }.freeze
15
17
  attr_reader :exemplar_reservoir
16
18
 
@@ -31,7 +33,7 @@ module OpenTelemetry
31
33
  @boundaries = boundaries && !boundaries.empty? ? boundaries.sort : nil
32
34
  @record_min_max = record_min_max
33
35
  @exemplar_reservoir = exemplar_reservoir || Metrics::Exemplar::AlignedHistogramBucketExemplarReservoir.new(boundaries: @boundaries)
34
- @exemplar_reservoir_storage = {}
36
+ @exemplar_reservoir_storage = new_stream_storage
35
37
  end
36
38
 
37
39
  # Returns the current histogram data points, clearing them for delta temporality.
@@ -41,8 +43,8 @@ module OpenTelemetry
41
43
  hdps = data_points.values.map! do |hdp|
42
44
  hdp.start_time_unix_nano = start_time
43
45
  hdp.time_unix_nano = end_time
44
- reservoir = @exemplar_reservoir_storage[hdp.attributes]
45
- hdp.exemplars = reservoir&.collect(attributes: hdp.attributes, aggregation_temporality: @aggregation_temporality)
46
+ reservoir = @exemplar_reservoir_storage[data_points][hdp.attributes]
47
+ hdp.exemplars = reservoir&.collect(attributes: hdp.attributes, aggregation_temporality: @aggregation_temporality.temporality)
46
48
  hdp
47
49
  end
48
50
  data_points.clear
@@ -52,8 +54,8 @@ module OpenTelemetry
52
54
  data_points.values.map! do |hdp|
53
55
  hdp.start_time_unix_nano ||= start_time # Start time of a data point is from the first observation.
54
56
  hdp.time_unix_nano = end_time
55
- reservoir = @exemplar_reservoir_storage[hdp.attributes]
56
- hdp.exemplars = reservoir&.collect(attributes: hdp.attributes, aggregation_temporality: @aggregation_temporality)
57
+ reservoir = @exemplar_reservoir_storage[data_points][hdp.attributes]
58
+ hdp.exemplars = reservoir&.collect(attributes: hdp.attributes, aggregation_temporality: @aggregation_temporality.temporality)
57
59
  hdp = hdp.dup
58
60
  hdp.bucket_counts = hdp.bucket_counts.dup
59
61
  hdp
@@ -71,7 +73,7 @@ module OpenTelemetry
71
73
  create_new_data_point(attributes, data_points)
72
74
  end
73
75
 
74
- update_histogram_data_point(hdp, amount, exemplar_offer: exemplar_offer)
76
+ update_histogram_data_point(hdp, amount, data_points, exemplar_offer: exemplar_offer)
75
77
  nil
76
78
  end
77
79
 
@@ -103,8 +105,8 @@ module OpenTelemetry
103
105
  )
104
106
  end
105
107
 
106
- def update_histogram_data_point(hdp, amount, exemplar_offer: false)
107
- reservior_update(hdp.attributes, amount, exemplar_offer)
108
+ def update_histogram_data_point(hdp, amount, stream_key, exemplar_offer: false)
109
+ reservior_update(hdp.attributes, amount, exemplar_offer, stream_key)
108
110
 
109
111
  if @record_min_max
110
112
  hdp.max = amount if amount > hdp.max
@@ -119,12 +121,12 @@ module OpenTelemetry
119
121
  hdp.bucket_counts[bucket_index] += 1
120
122
  end
121
123
 
122
- def reservior_update(attributes, amount, exemplar_offer)
123
- reservoir = @exemplar_reservoir_storage[attributes]
124
+ def reservior_update(attributes, amount, exemplar_offer, stream_key)
125
+ reservoir = @exemplar_reservoir_storage[stream_key][attributes]
124
126
  unless reservoir
125
127
  reservoir = @exemplar_reservoir.dup
126
128
  reservoir.reset
127
- @exemplar_reservoir_storage[attributes] = reservoir
129
+ @exemplar_reservoir_storage[stream_key][attributes] = reservoir
128
130
  end
129
131
 
130
132
  return unless exemplar_offer
@@ -17,6 +17,8 @@ module OpenTelemetry
17
17
  module Aggregation
18
18
  # Contains the implementation of the {https://opentelemetry.io/docs/specs/otel/metrics/data-model/#exponentialhistogram ExponentialBucketHistogram} aggregation
19
19
  class ExponentialBucketHistogram # rubocop:disable Metrics/ClassLength
20
+ include StreamScopedStorage
21
+
20
22
  OVERFLOW_ATTRIBUTE_SET = { 'otel.metric.overflow' => true }.freeze
21
23
 
22
24
  # relate to min max scale: https://opentelemetry.io/docs/specs/otel/metrics/sdk/#support-a-minimum-and-maximum-scale
@@ -29,10 +31,6 @@ module OpenTelemetry
29
31
 
30
32
  attr_reader :exemplar_reservoir
31
33
 
32
- # if no reservoir pass from instrument, then use this empty reservoir to avoid no method found error
33
- DEFAULT_RESERVOIR = Metrics::Exemplar::SimpleFixedSizeExemplarReservoir.new
34
- private_constant :DEFAULT_RESERVOIR
35
-
36
34
  # The default boundaries are calculated based on default max_size and max_scale values
37
35
  def initialize(
38
36
  aggregation_temporality: ENV.fetch('OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE', :delta),
@@ -53,46 +51,58 @@ module OpenTelemetry
53
51
  @size = validate_size(max_size)
54
52
  @scale = validate_scale(max_scale)
55
53
 
56
- @exemplar_reservoir = exemplar_reservoir || DEFAULT_RESERVOIR
57
- @exemplar_reservoir_storage = {}
54
+ @exemplar_reservoir = exemplar_reservoir || Metrics::Exemplar::SimpleFixedSizeExemplarReservoir.new(max_size: [20, @size].min)
55
+ @exemplar_reservoir_storage = new_stream_storage
58
56
 
59
57
  @mapping = new_mapping(@scale)
60
58
 
61
59
  # Previous state for cumulative aggregation
62
- @previous_positive = {} # nil
63
- @previous_negative = {} # nil
64
- @previous_min = {} # Float::INFINITY
65
- @previous_max = {} # -Float::INFINITY
66
- @previous_sum = {} # 0
67
- @previous_count = {} # 0
68
- @previous_zero_count = {} # 0
69
- @previous_scale = {} # nil
70
-
71
- # Cache mappings per attribute set
72
- @mappings = {}
73
- @previous_mappings = {}
60
+ @previous_positive = new_stream_storage # nil
61
+ @previous_negative = new_stream_storage # nil
62
+ @previous_min = new_stream_storage # Float::INFINITY
63
+ @previous_max = new_stream_storage # -Float::INFINITY
64
+ @previous_sum = new_stream_storage # 0
65
+ @previous_count = new_stream_storage # 0
66
+ @previous_zero_count = new_stream_storage # 0
67
+ @previous_scale = new_stream_storage # nil
68
+
69
+ @mappings = new_stream_storage
70
+ @previous_mappings = new_stream_storage
74
71
  end
75
72
 
76
73
  # when aggregation temporality is cumulative, merge and downscale will happen.
77
74
  # rubocop:disable-next Metrics/MethodLength
78
75
  def collect(start_time, end_time, data_points)
76
+ stream_key = data_points
77
+ stream_exemplar_reservoir_storage = @exemplar_reservoir_storage[stream_key]
78
+
79
79
  if @aggregation_temporality.delta?
80
80
  # Set timestamps and 'move' data point values to result.
81
81
  hdps = data_points.values.map! do |hdp|
82
82
  hdp.start_time_unix_nano = start_time
83
83
  hdp.time_unix_nano = end_time
84
- reservoir = @exemplar_reservoir_storage[hdp.attributes]
85
- hdp.exemplars = reservoir&.collect(attributes: hdp.attributes, aggregation_temporality: @aggregation_temporality)
84
+ reservoir = stream_exemplar_reservoir_storage[hdp.attributes]
85
+ hdp.exemplars = reservoir&.collect(attributes: hdp.attributes, aggregation_temporality: @aggregation_temporality.temporality)
86
86
  hdp
87
87
  end
88
88
  data_points.clear
89
- @mappings.clear
89
+ @mappings.delete(stream_key)
90
90
  hdps
91
91
  else
92
92
  # CUMULATIVE temporality - merge current data_points to previous data_points
93
93
  # and only keep the merged data_points in @previous_*
94
94
 
95
95
  merged_data_points = {}
96
+ stream_previous_positive = @previous_positive[stream_key]
97
+ stream_previous_negative = @previous_negative[stream_key]
98
+ stream_previous_min = @previous_min[stream_key]
99
+ stream_previous_max = @previous_max[stream_key]
100
+ stream_previous_sum = @previous_sum[stream_key]
101
+ stream_previous_count = @previous_count[stream_key]
102
+ stream_previous_zero_count = @previous_zero_count[stream_key]
103
+ stream_previous_scale = @previous_scale[stream_key]
104
+ stream_mappings = @mappings[stream_key]
105
+ stream_previous_mappings = @previous_mappings[stream_key]
96
106
 
97
107
  # this will slow down the operation especially if large amount of data_points present
98
108
  # but it should be fine since with cumulative, the data_points are merged into previous_* and not kept in data_points
@@ -108,25 +118,25 @@ module OpenTelemetry
108
118
  current_scale = hdp.scale
109
119
 
110
120
  # Setup previous positive, negative bucket and scale based on three different cases
111
- @previous_positive[attributes] = current_positive.copy_empty if @previous_positive[attributes].nil?
112
- @previous_negative[attributes] = current_negative.copy_empty if @previous_negative[attributes].nil?
113
- @previous_scale[attributes] = current_scale if @previous_scale[attributes].nil?
121
+ stream_previous_positive[attributes] = current_positive.copy_empty if stream_previous_positive[attributes].nil?
122
+ stream_previous_negative[attributes] = current_negative.copy_empty if stream_previous_negative[attributes].nil?
123
+ stream_previous_scale[attributes] = current_scale if stream_previous_scale[attributes].nil?
114
124
 
115
125
  # Determine minimum scale for merging
116
- min_scale = [@previous_scale[attributes], current_scale].min
126
+ min_scale = [stream_previous_scale[attributes], current_scale].min
117
127
 
118
128
  # Calculate ranges for positive and negative buckets
119
129
  low_positive, high_positive = get_low_high_previous_current(
120
- @previous_positive[attributes],
130
+ stream_previous_positive[attributes],
121
131
  current_positive,
122
- @previous_scale[attributes],
132
+ stream_previous_scale[attributes],
123
133
  current_scale,
124
134
  min_scale
125
135
  )
126
136
  low_negative, high_negative = get_low_high_previous_current(
127
- @previous_negative[attributes],
137
+ stream_previous_negative[attributes],
128
138
  current_negative,
129
- @previous_scale[attributes],
139
+ stream_previous_scale[attributes],
130
140
  current_scale,
131
141
  min_scale
132
142
  )
@@ -138,79 +148,79 @@ module OpenTelemetry
138
148
  ].min
139
149
 
140
150
  # Downscale previous buckets if necessary
141
- downscale_change = @previous_scale[attributes] - min_scale
142
- downscale(downscale_change, @previous_positive[attributes], @previous_negative[attributes])
151
+ downscale_change = stream_previous_scale[attributes] - min_scale
152
+ downscale(downscale_change, stream_previous_positive[attributes], stream_previous_negative[attributes])
143
153
 
144
154
  # Merge current buckets into previous buckets (kind like update); it's always :cumulative
145
- merge_buckets(@previous_positive[attributes], current_positive, current_scale, min_scale, @aggregation_temporality)
146
- merge_buckets(@previous_negative[attributes], current_negative, current_scale, min_scale, @aggregation_temporality)
155
+ merge_buckets(stream_previous_positive[attributes], current_positive, current_scale, min_scale, @aggregation_temporality)
156
+ merge_buckets(stream_previous_negative[attributes], current_negative, current_scale, min_scale, @aggregation_temporality)
147
157
 
148
158
  # initialize min, max, sum, count, zero_count for first time
149
- @previous_min[attributes] = Float::INFINITY if @previous_min[attributes].nil?
150
- @previous_max[attributes] = -Float::INFINITY if @previous_max[attributes].nil?
151
- @previous_sum[attributes] = 0 if @previous_sum[attributes].nil?
152
- @previous_count[attributes] = 0 if @previous_count[attributes].nil?
153
- @previous_zero_count[attributes] = 0 if @previous_zero_count[attributes].nil?
159
+ stream_previous_min[attributes] = Float::INFINITY if stream_previous_min[attributes].nil?
160
+ stream_previous_max[attributes] = -Float::INFINITY if stream_previous_max[attributes].nil?
161
+ stream_previous_sum[attributes] = 0 if stream_previous_sum[attributes].nil?
162
+ stream_previous_count[attributes] = 0 if stream_previous_count[attributes].nil?
163
+ stream_previous_zero_count[attributes] = 0 if stream_previous_zero_count[attributes].nil?
154
164
 
155
165
  # Update aggregated values
156
- @previous_min[attributes] = [@previous_min[attributes], current_min].min
157
- @previous_max[attributes] = [@previous_max[attributes], current_max].max
158
- @previous_sum[attributes] += current_sum
159
- @previous_count[attributes] += current_count
160
- @previous_zero_count[attributes] += current_zero_count
161
- @previous_scale[attributes] = min_scale
166
+ stream_previous_min[attributes] = [stream_previous_min[attributes], current_min].min
167
+ stream_previous_max[attributes] = [stream_previous_max[attributes], current_max].max
168
+ stream_previous_sum[attributes] += current_sum
169
+ stream_previous_count[attributes] += current_count
170
+ stream_previous_zero_count[attributes] += current_zero_count
171
+ stream_previous_scale[attributes] = min_scale
162
172
 
163
173
  # Create merged data point
164
- reservoir = @exemplar_reservoir_storage[attributes]
174
+ reservoir = stream_exemplar_reservoir_storage[attributes]
165
175
  merged_hdp = ExponentialHistogramDataPoint.new(
166
176
  attributes,
167
177
  start_time,
168
178
  end_time,
169
- @previous_count[attributes],
170
- @previous_sum[attributes],
171
- @previous_scale[attributes],
172
- @previous_zero_count[attributes],
173
- @previous_positive[attributes].dup,
174
- @previous_negative[attributes].dup,
179
+ stream_previous_count[attributes],
180
+ stream_previous_sum[attributes],
181
+ stream_previous_scale[attributes],
182
+ stream_previous_zero_count[attributes],
183
+ stream_previous_positive[attributes].dup,
184
+ stream_previous_negative[attributes].dup,
175
185
  0, # flags
176
- reservoir&.collect(attributes: attributes, aggregation_temporality: @aggregation_temporality), # exemplars
177
- @previous_min[attributes],
178
- @previous_max[attributes],
186
+ reservoir&.collect(attributes: attributes, aggregation_temporality: @aggregation_temporality.temporality), # exemplars
187
+ stream_previous_min[attributes],
188
+ stream_previous_max[attributes],
179
189
  @zero_threshold
180
190
  )
181
191
 
182
192
  merged_data_points[attributes] = merged_hdp
183
- @previous_mappings[attributes] = @mappings[attributes] if @mappings[attributes] # Preserve mapping for next collection
193
+ stream_previous_mappings[attributes] = stream_mappings[attributes] if stream_mappings[attributes] # Preserve mapping for next collection
184
194
  end
185
195
 
186
196
  # when you have no local_data_points, the loop from cumulative aggregation will not run
187
197
  # so return last merged data points if exists
188
- if data_points.empty? && !@previous_positive.empty?
189
- @previous_positive.each_key do |attributes|
190
- reservoir = @exemplar_reservoir_storage[attributes]
198
+ if data_points.empty? && !stream_previous_positive.empty?
199
+ stream_previous_positive.each_key do |attributes|
200
+ reservoir = stream_exemplar_reservoir_storage[attributes]
191
201
  merged_hdp = ExponentialHistogramDataPoint.new(
192
202
  attributes,
193
203
  start_time,
194
204
  end_time,
195
- @previous_count[attributes],
196
- @previous_sum[attributes],
197
- @previous_scale[attributes],
198
- @previous_zero_count[attributes],
199
- @previous_positive[attributes].dup,
200
- @previous_negative[attributes].dup,
205
+ stream_previous_count[attributes],
206
+ stream_previous_sum[attributes],
207
+ stream_previous_scale[attributes],
208
+ stream_previous_zero_count[attributes],
209
+ stream_previous_positive[attributes].dup,
210
+ stream_previous_negative[attributes].dup,
201
211
  0, # flags
202
- reservoir&.collect(attributes: attributes, aggregation_temporality: @aggregation_temporality), # exemplars
203
- @previous_min[attributes],
204
- @previous_max[attributes],
212
+ reservoir&.collect(attributes: attributes, aggregation_temporality: @aggregation_temporality.temporality), # exemplars
213
+ stream_previous_min[attributes],
214
+ stream_previous_max[attributes],
205
215
  @zero_threshold
206
216
  )
207
217
  merged_data_points[attributes] = merged_hdp
208
218
  end
209
219
  end
210
220
 
211
- # Swap current with previous mappings for next cycle
212
- @mappings = @previous_mappings
213
- @previous_mappings = {}
221
+ # Swap current with previous mappings for next cycle, for this stream only
222
+ @mappings[stream_key] = stream_previous_mappings
223
+ @previous_mappings[stream_key] = {}
214
224
 
215
225
  # clear data_points since the data is merged into previous_* already;
216
226
  # otherwise we will have duplicated data_points in the next collect
@@ -229,7 +239,7 @@ module OpenTelemetry
229
239
  create_new_data_point(attributes, data_points)
230
240
  end
231
241
 
232
- update_histogram_data_point(hdp, attributes, amount, exemplar_offer: exemplar_offer)
242
+ update_histogram_data_point(hdp, attributes, amount, data_points, exemplar_offer: exemplar_offer)
233
243
  nil
234
244
  end
235
245
 
@@ -265,8 +275,8 @@ module OpenTelemetry
265
275
  end
266
276
 
267
277
  # rubocop:disable-next Metrics/CyclomaticComplexity,Metrics/MethodLength
268
- def update_histogram_data_point(hdp, attributes, amount, exemplar_offer: false)
269
- reservior_update(attributes, amount, exemplar_offer)
278
+ def update_histogram_data_point(hdp, attributes, amount, stream_key, exemplar_offer: false)
279
+ reservior_update(attributes, amount, exemplar_offer, stream_key)
270
280
 
271
281
  if @record_min_max
272
282
  hdp.max = amount if amount > hdp.max
@@ -289,11 +299,11 @@ module OpenTelemetry
289
299
  # Reset scale to max_scale if transitioning from all-zeros to first non-zero value
290
300
  if buckets.counts == [0] && hdp.scale == 0 && hdp.count > hdp.zero_count
291
301
  hdp.scale = @scale
292
- @mappings.delete(attributes) # Clear any cached mapping
302
+ @mappings[stream_key].delete(attributes) # Clear any cached mapping
293
303
  end
294
304
 
295
305
  # Get or create mapping for this attribute set
296
- mapping = @mappings[attributes] ||= new_mapping(hdp.scale)
306
+ mapping = @mappings[stream_key][attributes] ||= new_mapping(hdp.scale)
297
307
  bucket_index = mapping.map_to_index(amount)
298
308
 
299
309
  rescaling_needed = false
@@ -320,7 +330,7 @@ module OpenTelemetry
320
330
  downscale(scale_change, hdp.positive, hdp.negative)
321
331
  new_scale = mapping.scale - scale_change
322
332
  mapping = new_mapping(new_scale)
323
- @mappings[attributes] = mapping # Update cache
333
+ @mappings[stream_key][attributes] = mapping # Update cache
324
334
  bucket_index = mapping.map_to_index(amount)
325
335
 
326
336
  OpenTelemetry.logger.debug "Rescaled with new scale #{new_scale} from #{low} and #{high}; bucket_index is updated to #{bucket_index}"
@@ -352,12 +362,12 @@ module OpenTelemetry
352
362
  buckets.grow(span + 1, @size)
353
363
  end
354
364
 
355
- def reservior_update(attributes, amount, exemplar_offer)
356
- reservoir = @exemplar_reservoir_storage[attributes]
365
+ def reservior_update(attributes, amount, exemplar_offer, stream_key)
366
+ reservoir = @exemplar_reservoir_storage[stream_key][attributes]
357
367
  unless reservoir
358
368
  reservoir = @exemplar_reservoir.dup
359
369
  reservoir.reset
360
- @exemplar_reservoir_storage[attributes] = reservoir
370
+ @exemplar_reservoir_storage[stream_key][attributes] = reservoir
361
371
  end
362
372
 
363
373
  return unless exemplar_offer
@@ -10,6 +10,8 @@ module OpenTelemetry
10
10
  module Aggregation
11
11
  # Contains the implementation of the LastValue aggregation
12
12
  class LastValue
13
+ include StreamScopedStorage
14
+
13
15
  OVERFLOW_ATTRIBUTE_SET = { 'otel.metric.overflow' => true }.freeze
14
16
  attr_reader :exemplar_reservoir
15
17
 
@@ -19,15 +21,17 @@ module OpenTelemetry
19
21
 
20
22
  def initialize(exemplar_reservoir: nil)
21
23
  @exemplar_reservoir = exemplar_reservoir || DEFAULT_RESERVOIR
22
- @exemplar_reservoir_storage = {}
24
+ @exemplar_reservoir_storage = new_stream_storage
23
25
  end
24
26
 
25
27
  # Returns the current data points, clearing them for delta temporality.
26
28
  def collect(start_time, end_time, data_points)
29
+ stream_exemplar_reservoir_storage = @exemplar_reservoir_storage[data_points]
30
+
27
31
  ndps = data_points.values.map! do |ndp|
28
32
  ndp.start_time_unix_nano = start_time
29
33
  ndp.time_unix_nano = end_time
30
- reservoir = @exemplar_reservoir_storage[ndp.attributes]
34
+ reservoir = stream_exemplar_reservoir_storage[ndp.attributes]
31
35
  ndp.exemplars = reservoir&.collect(attributes: ndp.attributes, aggregation_temporality: :delta)
32
36
  ndp
33
37
  end
@@ -46,7 +50,7 @@ module OpenTelemetry
46
50
  create_new_data_point(attributes, data_points)
47
51
  end
48
52
 
49
- update_number_data_point(ndp, increment, exemplar_offer: exemplar_offer)
53
+ update_number_data_point(ndp, increment, data_points, exemplar_offer: exemplar_offer)
50
54
  nil
51
55
  end
52
56
 
@@ -63,17 +67,17 @@ module OpenTelemetry
63
67
  )
64
68
  end
65
69
 
66
- def update_number_data_point(ndp, increment, exemplar_offer: false)
70
+ def update_number_data_point(ndp, increment, stream_key, exemplar_offer: false)
67
71
  ndp.value = increment
68
- reservior_update(ndp.attributes, increment, exemplar_offer)
72
+ reservior_update(ndp.attributes, increment, exemplar_offer, stream_key)
69
73
  end
70
74
 
71
- def reservior_update(attributes, increment, exemplar_offer)
72
- reservoir = @exemplar_reservoir_storage[attributes]
75
+ def reservior_update(attributes, increment, exemplar_offer, stream_key)
76
+ reservoir = @exemplar_reservoir_storage[stream_key][attributes]
73
77
  unless reservoir
74
78
  reservoir = @exemplar_reservoir.dup
75
79
  reservoir.reset
76
- @exemplar_reservoir_storage[attributes] = reservoir
80
+ @exemplar_reservoir_storage[stream_key][attributes] = reservoir
77
81
  end
78
82
 
79
83
  return unless exemplar_offer
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright The OpenTelemetry Authors
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ module OpenTelemetry
8
+ module SDK
9
+ module Metrics
10
+ module Aggregation
11
+ # Builds the nested storage aggregations use to keep per-stream state
12
+ # apart, since a wildcard or regex view shares one aggregation instance
13
+ # across every stream it matches.
14
+ module StreamScopedStorage
15
+ private
16
+
17
+ # Streams are keyed by their +data_points+ hash, which is mutable, so
18
+ # the outer hash must compare by identity rather than by value.
19
+ def new_stream_storage
20
+ Hash.new { |h, k| h[k] = {} }.compare_by_identity
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -10,6 +10,8 @@ module OpenTelemetry
10
10
  module Aggregation
11
11
  # Contains the implementation of the Sum aggregation
12
12
  class Sum
13
+ include StreamScopedStorage
14
+
13
15
  OVERFLOW_ATTRIBUTE_SET = { 'otel.metric.overflow' => true }.freeze
14
16
  attr_reader :exemplar_reservoir
15
17
 
@@ -24,18 +26,20 @@ module OpenTelemetry
24
26
  @aggregation_temporality = AggregationTemporality.determine_temporality(aggregation_temporality: aggregation_temporality, instrument_kind: instrument_kind, default: :cumulative)
25
27
  @monotonic = monotonic
26
28
  @exemplar_reservoir = exemplar_reservoir || DEFAULT_RESERVOIR
27
- @exemplar_reservoir_storage = {}
29
+ @exemplar_reservoir_storage = new_stream_storage
28
30
  end
29
31
 
30
32
  # Returns the current sum data points, clearing them for delta temporality.
31
33
  def collect(start_time, end_time, data_points)
34
+ stream_exemplar_reservoir_storage = @exemplar_reservoir_storage[data_points]
35
+
32
36
  if @aggregation_temporality.delta?
33
37
  # Set timestamps and 'move' data point values to result.
34
38
  ndps = data_points.values.map! do |ndp|
35
39
  ndp.start_time_unix_nano = start_time
36
40
  ndp.time_unix_nano = end_time
37
- reservoir = @exemplar_reservoir_storage[ndp.attributes]
38
- ndp.exemplars = reservoir&.collect(attributes: ndp.attributes, aggregation_temporality: @aggregation_temporality)
41
+ reservoir = stream_exemplar_reservoir_storage[ndp.attributes]
42
+ ndp.exemplars = reservoir&.collect(attributes: ndp.attributes, aggregation_temporality: @aggregation_temporality.temporality)
39
43
  ndp
40
44
  end
41
45
  data_points.clear
@@ -45,8 +49,8 @@ module OpenTelemetry
45
49
  data_points.values.map! do |ndp|
46
50
  ndp.start_time_unix_nano ||= start_time # Start time of a data point is from the first observation.
47
51
  ndp.time_unix_nano = end_time
48
- reservoir = @exemplar_reservoir_storage[ndp.attributes]
49
- ndp.exemplars = reservoir&.collect(attributes: ndp.attributes, aggregation_temporality: @aggregation_temporality)
52
+ reservoir = stream_exemplar_reservoir_storage[ndp.attributes]
53
+ ndp.exemplars = reservoir&.collect(attributes: ndp.attributes, aggregation_temporality: @aggregation_temporality.temporality)
50
54
  ndp.dup
51
55
  end
52
56
  end
@@ -65,7 +69,7 @@ module OpenTelemetry
65
69
  create_new_data_point(attributes, data_points)
66
70
  end
67
71
 
68
- update_number_data_point(ndp, increment, exemplar_offer: exemplar_offer)
72
+ update_number_data_point(ndp, increment, data_points, exemplar_offer: exemplar_offer)
69
73
  nil
70
74
  end
71
75
 
@@ -92,17 +96,17 @@ module OpenTelemetry
92
96
  )
93
97
  end
94
98
 
95
- def update_number_data_point(ndp, increment, exemplar_offer: false)
96
- reservior_update(ndp.attributes, increment, exemplar_offer)
99
+ def update_number_data_point(ndp, increment, stream_key, exemplar_offer: false)
100
+ reservior_update(ndp.attributes, increment, exemplar_offer, stream_key)
97
101
  ndp.value += increment
98
102
  end
99
103
 
100
- def reservior_update(attributes, increment, exemplar_offer)
101
- reservoir = @exemplar_reservoir_storage[attributes]
104
+ def reservior_update(attributes, increment, exemplar_offer, stream_key)
105
+ reservoir = @exemplar_reservoir_storage[stream_key][attributes]
102
106
  unless reservoir
103
107
  reservoir = @exemplar_reservoir.dup
104
108
  reservoir.reset
105
- @exemplar_reservoir_storage[attributes] = reservoir
109
+ @exemplar_reservoir_storage[stream_key][attributes] = reservoir
106
110
  end
107
111
 
108
112
  return unless exemplar_offer
@@ -16,6 +16,7 @@ module OpenTelemetry
16
16
  end
17
17
 
18
18
  require 'opentelemetry/sdk/metrics/aggregation/aggregation_temporality'
19
+ require 'opentelemetry/sdk/metrics/aggregation/stream_scoped_storage'
19
20
  require 'opentelemetry/sdk/metrics/aggregation/number_data_point'
20
21
  require 'opentelemetry/sdk/metrics/aggregation/histogram_data_point'
21
22
  require 'opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram'
@@ -12,12 +12,27 @@ module OpenTelemetry
12
12
  # It is not required to subclass this class to provide an implementation
13
13
  # of MetricReader, provided the interface is satisfied.
14
14
  class MetricReader
15
+ # Raised when a reader is registered with a second MeterProvider.
16
+ SINGLE_PROVIDER_ERROR = 'MetricReader cannot be registered with more than one MeterProvider'
17
+
18
+ private_constant :SINGLE_PROVIDER_ERROR
19
+
15
20
  attr_reader :metric_store
16
21
 
17
22
  def initialize(aggregation_cardinality_limit: nil)
23
+ @mutex = Mutex.new
18
24
  @metric_store = OpenTelemetry::SDK::Metrics::State::MetricStore.new(cardinality_limit: aggregation_cardinality_limit)
19
25
  end
20
26
 
27
+ # Registers this reader with a MeterProvider.
28
+ def register_meter_provider(meter_provider)
29
+ @mutex.synchronize do
30
+ raise ArgumentError, SINGLE_PROVIDER_ERROR if @meter_provider && !@meter_provider.equal?(meter_provider)
31
+
32
+ @meter_provider = meter_provider
33
+ end
34
+ end
35
+
21
36
  # Collects and returns the current metrics from the metric store.
22
37
  def collect
23
38
  @metric_store.collect
@@ -53,7 +53,9 @@ module OpenTelemetry
53
53
  # The API MUST support creation of asynchronous instruments by passing zero or more callback functions
54
54
  # to be permanently registered to the newly created instrument.
55
55
  def init_callback(callback)
56
- if callback.instance_of?(Proc)
56
+ if callback.nil?
57
+ nil # zero callbacks permanently registered at creation is permitted
58
+ elsif callback.instance_of?(Proc)
57
59
  @callbacks << callback
58
60
  elsif callback.instance_of?(Array)
59
61
  callback.each { |cb| @callbacks << cb if cb.instance_of?(Proc) }
@@ -63,12 +63,14 @@ module OpenTelemetry
63
63
  end
64
64
 
65
65
  # Validates the given instrument options and creates the instrument of the given kind.
66
- def create_instrument(kind, name, unit, description, callback, exemplar_filter, exemplar_reservoir)
66
+ # TODO: advisory parameters are accepted but not yet applied to stream configuration.
67
+ def create_instrument(kind, name, unit, description, callback, exemplar_filter, exemplar_reservoir, advisory)
67
68
  raise InstrumentNameError if name.nil?
68
69
  raise InstrumentNameError if name.empty?
69
70
  raise InstrumentNameError unless NAME_REGEX.match?(name)
70
- raise InstrumentUnitError if unit && (!unit.ascii_only? || unit.size > 63)
71
- raise InstrumentDescriptionError if description && (description.size > 1023 || !utf8mb3_encoding?(description.dup))
71
+
72
+ unit ||= ''
73
+ description ||= ''
72
74
 
73
75
  super do
74
76
  case kind
@@ -82,12 +84,6 @@ module OpenTelemetry
82
84
  end
83
85
  end
84
86
  end
85
-
86
- # Returns whether string is valid UTF-8 with no 4-byte (utf8mb4) characters.
87
- def utf8mb3_encoding?(string)
88
- string.force_encoding('UTF-8').valid_encoding? &&
89
- string.each_char { |c| return false if c.bytesize >= 4 }
90
- end
91
87
  end
92
88
  end
93
89
  end
@@ -13,6 +13,11 @@ module OpenTelemetry
13
13
  # rubocop:disable-next Metrics/ClassLength
14
14
  class MeterProvider < OpenTelemetry::Metrics::MeterProvider
15
15
  EMPTY_ATTRIBUTES = {}.freeze
16
+ READER_OWNERS = ObjectSpace::WeakMap.new
17
+ READER_OWNERS_MUTEX = Mutex.new
18
+ READER_OWNERSHIP_ERROR = 'MetricReader cannot be registered with more than one MeterProvider'
19
+
20
+ private_constant :READER_OWNERS, :READER_OWNERS_MUTEX, :READER_OWNERSHIP_ERROR
16
21
 
17
22
  Key = Struct.new(:name, :version, :attributes)
18
23
  private_constant(:Key)
@@ -118,6 +123,7 @@ module OpenTelemetry
118
123
  if @stopped
119
124
  OpenTelemetry.logger.warn('calling MetricProvider#add_metric_reader after shutdown.')
120
125
  else
126
+ register_metric_reader(metric_reader)
121
127
  @metric_readers.push(metric_reader)
122
128
  @meter_registry.each_value { |meter| meter.add_metric_reader(metric_reader) }
123
129
  end
@@ -126,6 +132,18 @@ module OpenTelemetry
126
132
  end
127
133
  end
128
134
 
135
+ def register_metric_reader(metric_reader)
136
+ READER_OWNERS_MUTEX.synchronize do
137
+ owner = READER_OWNERS[metric_reader]
138
+ raise ArgumentError, READER_OWNERSHIP_ERROR if owner && !owner.equal?(self)
139
+
140
+ READER_OWNERS[metric_reader] = self
141
+ end
142
+
143
+ metric_reader.register_meter_provider(self) if metric_reader.respond_to?(:register_meter_provider)
144
+ end
145
+ private :register_metric_reader
146
+
129
147
  # @api private
130
148
  def register_synchronous_instrument(instrument)
131
149
  @mutex.synchronize do
@@ -8,7 +8,7 @@ module OpenTelemetry
8
8
  module SDK
9
9
  module Metrics
10
10
  # Current OpenTelemetry metrics sdk version
11
- VERSION = '0.18.0'
11
+ VERSION = '0.19.0'
12
12
  end
13
13
  end
14
14
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opentelemetry-metrics-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.0
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenTelemetry Authors
@@ -78,6 +78,7 @@ files:
78
78
  - lib/opentelemetry/sdk/metrics/aggregation/histogram_data_point.rb
79
79
  - lib/opentelemetry/sdk/metrics/aggregation/last_value.rb
80
80
  - lib/opentelemetry/sdk/metrics/aggregation/number_data_point.rb
81
+ - lib/opentelemetry/sdk/metrics/aggregation/stream_scoped_storage.rb
81
82
  - lib/opentelemetry/sdk/metrics/aggregation/sum.rb
82
83
  - lib/opentelemetry/sdk/metrics/configurator_patch.rb
83
84
  - lib/opentelemetry/sdk/metrics/exemplar.rb
@@ -121,10 +122,10 @@ homepage: https://github.com/open-telemetry/opentelemetry-ruby
121
122
  licenses:
122
123
  - Apache-2.0
123
124
  metadata:
124
- changelog_uri: https://rubydoc.info/gems/opentelemetry-metrics-sdk/0.18.0/file/CHANGELOG.md
125
- source_code_uri: https://github.com/open-telemetry/opentelemetry-ruby/tree/opentelemetry-metrics-sdk/v0.18.0/metrics_sdk
125
+ changelog_uri: https://rubydoc.info/gems/opentelemetry-metrics-sdk/0.19.0/file/CHANGELOG.md
126
+ source_code_uri: https://github.com/open-telemetry/opentelemetry-ruby/tree/opentelemetry-metrics-sdk/v0.19.0/metrics_sdk
126
127
  bug_tracker_uri: https://github.com/open-telemetry/opentelemetry-ruby/issues
127
- documentation_uri: https://rubydoc.info/gems/opentelemetry-metrics-sdk/0.18.0
128
+ documentation_uri: https://rubydoc.info/gems/opentelemetry-metrics-sdk/0.19.0
128
129
  rdoc_options: []
129
130
  require_paths:
130
131
  - lib
@@ -139,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
140
  - !ruby/object:Gem::Version
140
141
  version: '0'
141
142
  requirements: []
142
- rubygems_version: 4.0.16
143
+ rubygems_version: 4.0.20
143
144
  specification_version: 4
144
145
  summary: A stats collection and distributed tracing framework
145
146
  test_files: []