@@ -19,6 +19,7 @@ package prometheusremotewrite
19
19
import (
20
20
"context"
21
21
"fmt"
22
+ "github.com/prometheus/prometheus/model/histogram"
22
23
"math"
23
24
24
25
"github.com/prometheus/common/model"
@@ -89,8 +90,8 @@ func exponentialToNativeHistogram(p pmetric.ExponentialHistogramDataPoint) (prom
89
90
scale = 8
90
91
}
91
92
92
- pSpans , pDeltas := convertBucketsLayout (p .Positive (), scaleDown )
93
- nSpans , nDeltas := convertBucketsLayout (p .Negative (), scaleDown )
93
+ pSpans , pDeltas := convertBucketsLayout (p .Positive (). BucketCounts (). AsRaw (), p . Positive (). Offset (), scaleDown , true )
94
+ nSpans , nDeltas := convertBucketsLayout (p .Negative (). BucketCounts (). AsRaw (), p . Negative (). Offset (), scaleDown , true )
94
95
95
96
h := prompb.Histogram {
96
97
// The counter reset detection must be compatible with Prometheus to
@@ -133,19 +134,25 @@ func exponentialToNativeHistogram(p pmetric.ExponentialHistogramDataPoint) (prom
133
134
return h , annots , nil
134
135
}
135
136
136
- // convertBucketsLayout translates OTel Exponential Histogram dense buckets
137
- // representation to Prometheus Native Histogram sparse bucket representation.
137
+ // convertBucketsLayout translates OTel Explicit or Exponential Histogram dense buckets
138
+ // representation to Prometheus Native Histogram sparse bucket representation. This is used
139
+ // for translating Exponential Histograms into Native Histograms, and Explicit Histograms
140
+ // into Native Histograms with Custom Buckets.
138
141
//
139
142
// The translation logic is taken from the client_golang `histogram.go#makeBuckets`
140
143
// function, see `makeBuckets` https://github.com/prometheus/client_golang/blob/main/prometheus/histogram.go
141
- // The bucket indexes conversion was adjusted, since OTel exp. histogram bucket
144
+ //
145
+ // scaleDown is the factor by which the buckets are scaled down. In other words 2^scaleDown buckets will be merged into one.
146
+ //
147
+ // When converting from OTel Exponential Histograms to Native Histograms, the
148
+ // bucket indexes conversion is adjusted, since OTel exp. histogram bucket
142
149
// index 0 corresponds to the range (1, base] while Prometheus bucket index 0
143
150
// to the range (base 1].
144
151
//
145
- // scaleDown is the factor by which the buckets are scaled down. In other words 2^scaleDown buckets will be merged into one.
146
- func convertBucketsLayout ( buckets pmetric. ExponentialHistogramDataPointBuckets , scaleDown int32 ) ([]prompb. BucketSpan , [] int64 ) {
147
- bucketCounts := buckets . BucketCounts ()
148
- if bucketCounts . Len ( ) == 0 {
152
+ // When converting from OTel Explicit Histograms to Native Histograms with Custom Buckets,
153
+ // the bucket indexes are not scaled, and the indices are not adjusted by 1.
154
+ func convertBucketsLayout ( bucketCounts [] uint64 , offset int32 , scaleDown int32 , adjustOffset bool ) ([]prompb. BucketSpan , [] int64 ) {
155
+ if len ( bucketCounts ) == 0 {
149
156
return nil , nil
150
157
}
151
158
@@ -164,24 +171,28 @@ func convertBucketsLayout(buckets pmetric.ExponentialHistogramDataPointBuckets,
164
171
165
172
// Let the compiler figure out that this is const during this function by
166
173
// moving it into a local variable.
167
- numBuckets := bucketCounts .Len ()
174
+ numBuckets := len (bucketCounts )
175
+
176
+ bucketIdx := offset >> scaleDown + 1
177
+
178
+ initialOffset := offset
179
+ if adjustOffset {
180
+ initialOffset = initialOffset >> scaleDown + 1
181
+ }
168
182
169
- // The offset is scaled and adjusted by 1 as described above.
170
- bucketIdx := buckets .Offset ()>> scaleDown + 1
171
183
spans = append (spans , prompb.BucketSpan {
172
- Offset : bucketIdx ,
184
+ Offset : initialOffset ,
173
185
Length : 0 ,
174
186
})
175
187
176
188
for i := 0 ; i < numBuckets ; i ++ {
177
- // The offset is scaled and adjusted by 1 as described above.
178
- nextBucketIdx := (int32 (i )+ buckets .Offset ())>> scaleDown + 1
189
+ nextBucketIdx := (int32 (i )+ offset )>> scaleDown + 1
179
190
if bucketIdx == nextBucketIdx { // We have not collected enough buckets to merge yet.
180
- count += int64 (bucketCounts . At ( i ) )
191
+ count += int64 (bucketCounts [ i ] )
181
192
continue
182
193
}
183
194
if count == 0 {
184
- count = int64 (bucketCounts . At ( i ) )
195
+ count = int64 (bucketCounts [ i ] )
185
196
continue
186
197
}
187
198
@@ -202,11 +213,12 @@ func convertBucketsLayout(buckets pmetric.ExponentialHistogramDataPointBuckets,
202
213
}
203
214
}
204
215
appendDelta (count )
205
- count = int64 (bucketCounts . At ( i ) )
216
+ count = int64 (bucketCounts [ i ] )
206
217
bucketIdx = nextBucketIdx
207
218
}
219
+
208
220
// Need to use the last item's index. The offset is scaled and adjusted by 1 as described above.
209
- gap := (int32 (numBuckets )+ buckets . Offset () - 1 )>> scaleDown + 1 - bucketIdx
221
+ gap := (int32 (numBuckets )+ offset - 1 )>> scaleDown + 1 - bucketIdx
210
222
if gap > 2 {
211
223
// We have to create a new span, because we have found a gap
212
224
// of more than two buckets. The constant 2 is copied from the logic in
@@ -226,3 +238,100 @@ func convertBucketsLayout(buckets pmetric.ExponentialHistogramDataPointBuckets,
226
238
227
239
return spans , deltas
228
240
}
241
+
242
+ func (c * PrometheusConverter ) addCustomBucketsHistogramDataPoints (ctx context.Context , dataPoints pmetric.HistogramDataPointSlice ,
243
+ resource pcommon.Resource , settings Settings , promName string ) (annotations.Annotations , error ) {
244
+ var annots annotations.Annotations
245
+
246
+ for x := 0 ; x < dataPoints .Len (); x ++ {
247
+ if err := c .everyN .checkContext (ctx ); err != nil {
248
+ return annots , err
249
+ }
250
+
251
+ pt := dataPoints .At (x )
252
+
253
+ histogram , ws , err := explicitHistogramToCustomBucketsHistogram (pt )
254
+ annots .Merge (ws )
255
+ if err != nil {
256
+ return annots , err
257
+ }
258
+
259
+ lbls := createAttributes (
260
+ resource ,
261
+ pt .Attributes (),
262
+ settings ,
263
+ nil ,
264
+ true ,
265
+ model .MetricNameLabel ,
266
+ promName ,
267
+ )
268
+
269
+ ts , _ := c .getOrCreateTimeSeries (lbls )
270
+ ts .Histograms = append (ts .Histograms , histogram )
271
+
272
+ exemplars , err := getPromExemplars [pmetric.HistogramDataPoint ](ctx , & c .everyN , pt )
273
+ if err != nil {
274
+ return annots , err
275
+ }
276
+ ts .Exemplars = append (ts .Exemplars , exemplars ... )
277
+ }
278
+
279
+ return annots , nil
280
+ }
281
+
282
+ func explicitHistogramToCustomBucketsHistogram (p pmetric.HistogramDataPoint ) (prompb.Histogram , annotations.Annotations , error ) {
283
+ var annots annotations.Annotations
284
+
285
+ buckets := p .BucketCounts ().AsRaw ()
286
+ offset := getBucketOffset (buckets )
287
+ bucketCounts := buckets [offset :]
288
+ positiveSpans , positiveDeltas := convertBucketsLayout (bucketCounts , int32 (offset ), 0 , false )
289
+
290
+ // TODO(carrieedwards): Add setting to limit maximum bucket count
291
+ h := prompb.Histogram {
292
+ // The counter reset detection must be compatible with Prometheus to
293
+ // safely set ResetHint to NO. This is not ensured currently.
294
+ // Sending a sample that triggers counter reset but with ResetHint==NO
295
+ // would lead to Prometheus panic as it does not double check the hint.
296
+ // Thus we're explicitly saying UNKNOWN here, which is always safe.
297
+ // TODO: using created time stamp should be accurate, but we
298
+ // need to know here if it was used for the detection.
299
+ // Ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/28663#issuecomment-1810577303
300
+ // Counter reset detection in Prometheus: https://github.com/prometheus/prometheus/blob/f997c72f294c0f18ca13fa06d51889af04135195/tsdb/chunkenc/histogram.go#L232
301
+ ResetHint : prompb .Histogram_UNKNOWN ,
302
+ Schema : histogram .CustomBucketsSchema ,
303
+
304
+ PositiveSpans : positiveSpans ,
305
+ PositiveDeltas : positiveDeltas ,
306
+ // Note: OTel explicit histograms have an implicit +Inf bucket, which has a lower bound
307
+ // of the last element in the explicit_bounds array.
308
+ // This is similar to the custom_values array in native histograms with custom buckets.
309
+ // Because of this shared property, the OTel explicit histogram's explicit_bounds array
310
+ // can be mapped directly to the custom_values array.
311
+ // See: https://github.com/open-telemetry/opentelemetry-proto/blob/d7770822d70c7bd47a6891fc9faacc66fc4af3d3/opentelemetry/proto/metrics/v1/metrics.proto#L469
312
+ CustomValues : p .ExplicitBounds ().AsRaw (),
313
+
314
+ Timestamp : convertTimeStamp (p .Timestamp ()),
315
+ }
316
+
317
+ if p .Flags ().NoRecordedValue () {
318
+ h .Sum = math .Float64frombits (value .StaleNaN )
319
+ h .Count = & prompb.Histogram_CountInt {CountInt : value .StaleNaN }
320
+ } else {
321
+ if p .HasSum () {
322
+ h .Sum = p .Sum ()
323
+ }
324
+ h .Count = & prompb.Histogram_CountInt {CountInt : p .Count ()}
325
+ if p .Count () == 0 && h .Sum != 0 {
326
+ annots .Add (fmt .Errorf ("histogram data point has zero count, but non-zero sum: %f" , h .Sum ))
327
+ }
328
+ }
329
+ return h , annots , nil
330
+ }
331
+
332
+ func getBucketOffset (buckets []uint64 ) (offset int ) {
333
+ for offset < len (buckets ) && buckets [offset ] == 0 {
334
+ offset ++
335
+ }
336
+ return offset
337
+ }
0 commit comments