Skip to content

Commit e2822ba

Browse files
JustasLbryce-b
andauthored
Add instrument advisory API to "Stable" Metrics SDK (#793)
* Add advisory bucket size to Stable SDK Histogram * Add tests * Reverted unrelated changes * Updated test to verify exported data uses custom buckets --------- Co-authored-by: Bryce Buchanan <[email protected]>
1 parent 4012768 commit e2822ba

File tree

9 files changed

+120
-6
lines changed

9 files changed

+120
-6
lines changed

Sources/OpenTelemetryApi/Metrics/Stable/DefaultStableMeter.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ public class DefaultStableMeter: StableMeter {
4949
}
5050

5151
public class NoopDoubleHistogramBuilder: DoubleHistogramBuilder {
52+
public func setExplicitBucketBoundariesAdvice(_ boundaries: [Double]) -> Self {
53+
return self
54+
}
55+
5256
public func ofLongs() -> NoopLongHistogramBuilder {
5357
NoopLongHistogramBuilder()
5458
}
@@ -127,6 +131,10 @@ public class DefaultStableMeter: StableMeter {
127131
}
128132

129133
public class NoopLongHistogramBuilder: LongHistogramBuilder {
134+
public func setExplicitBucketBoundariesAdvice(_ boundaries: [Double]) -> Self {
135+
return self
136+
}
137+
130138
public func build() -> NoopLongHistogram {
131139
NoopLongHistogram()
132140
}

Sources/OpenTelemetryApi/Metrics/Stable/DoubleHistogramBuilder.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ public protocol DoubleHistogramBuilder: AnyObject {
99
associatedtype AnyLongHistogramBuilder : LongHistogramBuilder
1010
associatedtype AnyDoubleHistogram : DoubleHistogram
1111
func ofLongs() -> AnyLongHistogramBuilder
12+
13+
/// Sets explicit bucket boundaries advice for the histogram.
14+
/// This is a hint to the SDK about the recommended bucket boundaries.
15+
/// The SDK may ignore this advice if a View is configured for this instrument.
16+
/// - Parameter boundaries: Array of bucket boundaries in ascending order
17+
/// - Returns: Self for method chaining
18+
func setExplicitBucketBoundariesAdvice(_ boundaries: [Double]) -> Self
1219

1320
func build() -> AnyDoubleHistogram
1421
}

Sources/OpenTelemetryApi/Metrics/Stable/LongHistogramBuilder.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,13 @@ import Foundation
77

88
public protocol LongHistogramBuilder: AnyObject {
99
associatedtype AnyLongHistogram: LongHistogram
10+
11+
/// Sets explicit bucket boundaries advice for the histogram.
12+
/// This is a hint to the SDK about the recommended bucket boundaries.
13+
/// The SDK may ignore this advice if a View is configured for this instrument.
14+
/// - Parameter boundaries: Array of bucket boundaries in ascending order
15+
/// - Returns: Self for method chaining
16+
func setExplicitBucketBoundariesAdvice(_ boundaries: [Double]) -> Self
17+
1018
func build() -> AnyLongHistogram
1119
}

Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/DefaultAggregation.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ public class DefaultAggregation: Aggregation {
2121
case .counter, .upDownCounter, .observableCounter, .observableUpDownCounter:
2222
return SumAggregation.instance
2323
case .histogram:
24-
return ExplicitBucketHistogramAggregation.instance
24+
// Use advisory bucket boundaries if available, otherwise use default
25+
if let advisoryBoundaries = instrument.explicitBucketBoundariesAdvice {
26+
return ExplicitBucketHistogramAggregation(bucketBoundaries: advisoryBoundaries)
27+
} else {
28+
return ExplicitBucketHistogramAggregation.instance
29+
}
2530
case .observableGauge, .gauge:
2631
return LastValueAggregation.instance
2732
}

Sources/OpenTelemetrySdk/Metrics/Stable/DoubleHistogramMeterBuilderSdk.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ public class DoubleHistogramMeterBuilderSdk: InstrumentBuilder, DoubleHistogramB
2323
)
2424
}
2525

26+
public func setExplicitBucketBoundariesAdvice(_ boundaries: [Double]) -> Self {
27+
self.explicitBucketBoundariesAdvice = boundaries
28+
return self
29+
}
30+
2631
public func ofLongs() -> LongHistogramMeterBuilderSdk {
2732
swapBuilder(LongHistogramMeterBuilderSdk.init)
2833
}

Sources/OpenTelemetrySdk/Metrics/Stable/InstrumentBuilder.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public class InstrumentBuilder {
1414
var description: String
1515
var unit: String
1616
var instrumentName: String
17+
var explicitBucketBoundariesAdvice: [Double]?
18+
1719
init(meterProviderSharedState: inout MeterProviderSharedState, meterSharedState: inout StableMeterSharedState, type: InstrumentType, valueType: InstrumentValueType, description: String, unit: String, instrumentName: String) {
1820
self.meterProviderSharedState = meterProviderSharedState
1921
self.meterSharedState = meterSharedState
@@ -22,6 +24,7 @@ public class InstrumentBuilder {
2224
self.description = description
2325
self.unit = unit
2426
self.instrumentName = instrumentName
27+
self.explicitBucketBoundariesAdvice = nil
2528
}
2629
}
2730

@@ -38,12 +41,14 @@ public extension InstrumentBuilder {
3841
}
3942

4043
internal func swapBuilder<T: InstrumentBuilder>(_ builder: (inout MeterProviderSharedState, inout StableMeterSharedState, String, String, String) -> T) -> T {
41-
return builder(&meterProviderSharedState, &meterSharedState, instrumentName, description, unit)
44+
let newBuilder = builder(&meterProviderSharedState, &meterSharedState, instrumentName, description, unit)
45+
newBuilder.explicitBucketBoundariesAdvice = self.explicitBucketBoundariesAdvice
46+
return newBuilder
4247
}
4348

4449
// todo : Is it necessary to use inout for writableMetricStorage?
4550
func buildSynchronousInstrument<T: Instrument>(_ instrumentFactory: (InstrumentDescriptor, WritableMetricStorage) -> T) -> T {
46-
let descriptor = InstrumentDescriptor(name: instrumentName, description: description, unit: unit, type: type, valueType: valueType)
51+
let descriptor = InstrumentDescriptor(name: instrumentName, description: description, unit: unit, type: type, valueType: valueType, explicitBucketBoundariesAdvice: explicitBucketBoundariesAdvice)
4752
let storage = meterSharedState.registerSynchronousMetricStorage(instrument: descriptor, meterProviderSharedState: meterProviderSharedState)
4853
return instrumentFactory(descriptor, storage)
4954
}
@@ -67,7 +72,7 @@ public extension InstrumentBuilder {
6772
}
6873

6974
func buildObservableMeasurement(type: InstrumentType) -> StableObservableMeasurementSdk {
70-
let descriptor = InstrumentDescriptor(name: instrumentName, description: description, unit: unit, type: type, valueType: valueType)
75+
let descriptor = InstrumentDescriptor(name: instrumentName, description: description, unit: unit, type: type, valueType: valueType, explicitBucketBoundariesAdvice: explicitBucketBoundariesAdvice)
7176
return meterSharedState.registerObservableMeasurement(instrumentDescriptor: descriptor)
7277
}
7378
}

Sources/OpenTelemetrySdk/Metrics/Stable/InstrumentDescriptor.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,23 @@ public struct InstrumentDescriptor: Equatable {
1111
public let unit: String
1212
public let type: InstrumentType
1313
public let valueType: InstrumentValueType
14+
public let explicitBucketBoundariesAdvice: [Double]?
1415

15-
public init(name: String, description: String, unit: String, type: InstrumentType, valueType: InstrumentValueType) {
16+
public init(name: String, description: String, unit: String, type: InstrumentType, valueType: InstrumentValueType, explicitBucketBoundariesAdvice: [Double]? = nil) {
1617
self.name = name
1718
self.description = description
1819
self.unit = unit
1920
self.type = type
2021
self.valueType = valueType
22+
self.explicitBucketBoundariesAdvice = explicitBucketBoundariesAdvice
2123
}
2224

2325
public static func == (lhs: Self, rhs: Self) -> Bool {
2426
return lhs.name == rhs.name &&
2527
lhs.description == rhs.description &&
2628
lhs.unit == rhs.unit &&
2729
lhs.valueType == rhs.valueType &&
28-
lhs.type == rhs.type
30+
lhs.type == rhs.type &&
31+
lhs.explicitBucketBoundariesAdvice == rhs.explicitBucketBoundariesAdvice
2932
}
3033
}

Sources/OpenTelemetrySdk/Metrics/Stable/LongHistogramMeterBuilderSdk.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ public class LongHistogramMeterBuilderSdk: InstrumentBuilder, LongHistogramBuild
2323
)
2424
}
2525

26+
public func setExplicitBucketBoundariesAdvice(_ boundaries: [Double]) -> Self {
27+
self.explicitBucketBoundariesAdvice = boundaries
28+
return self
29+
}
30+
2631
public func build() -> LongHistogramMeterSdk {
2732
buildSynchronousInstrument(LongHistogramMeterSdk.init)
2833
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import XCTest
7+
import OpenTelemetryApi
8+
@testable import OpenTelemetrySdk
9+
10+
class AdvisoryParameterTests: XCTestCase {
11+
12+
func testHistogramExportedDataUsesCustomBucketsWithWaitingExporter() {
13+
let waitingExporter = WaitingMetricExporter(numberToWaitFor: 1, aggregationTemporality: .delta)
14+
let stableMeterProvider = StableMeterProviderSdk.builder()
15+
.registerMetricReader(reader: StablePeriodicMetricReaderSdk(exporter: waitingExporter, exportInterval: 5.0))
16+
.registerView(selector: InstrumentSelectorBuilder().build(), view: StableView.builder().build())
17+
.build()
18+
19+
let meter = stableMeterProvider.meterBuilder(name: "testMeter").build()
20+
21+
let customBoundaries = [1.0, 5.0, 10.0, 25.0, 50.0, 100.0]
22+
23+
let histogram = meter
24+
.histogramBuilder(name: "test_histogram_with_custom_buckets")
25+
.setExplicitBucketBoundariesAdvice(customBoundaries)
26+
.build()
27+
28+
// Record some values to generate data
29+
histogram.record(value: 3.0) // Should fall in bucket [1.0, 5.0)
30+
histogram.record(value: 7.0) // Should fall in bucket [5.0, 10.0)
31+
histogram.record(value: 15.0) // Should fall in bucket [10.0, 25.0)
32+
histogram.record(value: 75.0) // Should fall in bucket [50.0, 100.0)
33+
histogram.record(value: 150.0) // Should fall in the overflow bucket (>100.0)
34+
35+
// Wait for export using the waiting exporter
36+
let metrics = waitingExporter.waitForExport()
37+
38+
// Verify that we received the expected metric
39+
XCTAssertEqual(metrics.count, 1)
40+
41+
let metricData = metrics[0]
42+
XCTAssertEqual(metricData.name, "test_histogram_with_custom_buckets")
43+
XCTAssertEqual(metricData.type, .Histogram)
44+
45+
// Get the histogram data and verify the boundaries
46+
let histogramData = metricData.getHistogramData()
47+
XCTAssertEqual(histogramData.count, 1)
48+
49+
let pointData = histogramData[0]
50+
51+
// Verify that the exported data uses our custom boundaries
52+
XCTAssertEqual(pointData.boundaries, customBoundaries)
53+
54+
// Verify the bucket counts match our recorded values
55+
// Expected bucket counts: [0, 1, 1, 1, 0, 1, 1]
56+
// (empty bucket <1.0, then 1 value in each: [1-5), [5-10), [10-25), empty [25-50), 1 in [50-100), 1 in overflow >100)
57+
let expectedCounts = [0, 1, 1, 1, 0, 1, 1]
58+
XCTAssertEqual(pointData.counts, expectedCounts)
59+
60+
// Verify total count and sum
61+
XCTAssertEqual(pointData.count, 5)
62+
XCTAssertEqual(pointData.sum, 250.0) // 3 + 7 + 15 + 75 + 150
63+
64+
// Verify min and max
65+
XCTAssertEqual(pointData.min, 3.0)
66+
XCTAssertEqual(pointData.max, 150.0)
67+
}
68+
}

0 commit comments

Comments
 (0)