Skip to content

Commit 32b7b1e

Browse files
mamuntonachoBonafontebryce-b
authored
Added BatchSpanProcessor metrics on drop and export span (#635)
* added metrics for drop and export Span processor * added span size gauge * addressed PR comments * Tried to fix the indent * addressed PR comment --------- Co-authored-by: Ignacio Bonafonte <[email protected]> Co-authored-by: Bryce Buchanan <[email protected]>
1 parent 13c3857 commit 32b7b1e

File tree

4 files changed

+176
-60
lines changed

4 files changed

+176
-60
lines changed

Sources/OpenTelemetryApi/Metrics/Stable/DefaultStableMeter.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public class DefaultStableMeter : StableMeter {
6060
}
6161
}
6262

63-
private class NoopLongGaugeBuilder : LongGaugeBuilder {
63+
private class NoopLongGaugeBuilder : LongGaugeBuilder {
6464
func buildWithCallback(_ callback: @escaping (ObservableLongMeasurement) -> Void) -> ObservableLongGauge {
6565
NoopObservableLongGauge()
6666
}
@@ -127,11 +127,11 @@ public class DefaultStableMeter : StableMeter {
127127
func ofDoubles() -> DoubleCounterBuilder {
128128
NoopDoubleCounterBuilder()
129129
}
130-
130+
131131
func build() -> LongCounter {
132132
NoopLongCounter()
133133
}
134-
134+
135135
func buildWithCallback(_ callback: @escaping (ObservableLongMeasurement) -> Void) -> ObservableLongCounter {
136136
NoopObservableLongCounter()
137137
}

Sources/OpenTelemetrySdk/Metrics/Stable/LongGaugeBuilderSdk.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,4 @@ public class LongGaugeBuilderSdk : LongGaugeBuilder, InstrumentBuilder {
3333
public func buildWithCallback(_ callback: @escaping (OpenTelemetryApi.ObservableLongMeasurement) -> Void) -> OpenTelemetryApi.ObservableLongGauge {
3434
registerLongAsynchronousInstrument(type: type, updater: callback)
3535
}
36-
37-
38-
39-
4036
}

Sources/OpenTelemetrySdk/Trace/SpanProcessors/BatchSpanProcessor.swift

Lines changed: 130 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,36 @@ import OpenTelemetryApi
1515
/// exports the spans to wake up and start a new export cycle.
1616
/// This batchSpanProcessor can cause high contention in a very high traffic service.
1717
public struct BatchSpanProcessor: SpanProcessor {
18+
fileprivate static let SPAN_PROCESSOR_TYPE_LABEL: String = "processorType"
19+
fileprivate static let SPAN_PROCESSOR_DROPPED_LABEL: String = "dropped"
20+
fileprivate static let SPAN_PROCESSOR_TYPE_VALUE: String = BatchSpanProcessor.name
21+
22+
fileprivate var worker: BatchWorker
1823

19-
20-
fileprivate var worker: BatchWorker
21-
22-
public init(spanExporter: SpanExporter, scheduleDelay: TimeInterval = 5, exportTimeout: TimeInterval = 30,
23-
maxQueueSize: Int = 2048, maxExportBatchSize: Int = 512, willExportCallback: ((inout [SpanData]) -> Void)? = nil)
24-
{
25-
worker = BatchWorker(spanExporter: spanExporter,
26-
scheduleDelay: scheduleDelay,
27-
exportTimeout: exportTimeout,
28-
maxQueueSize: maxQueueSize,
29-
maxExportBatchSize: maxExportBatchSize,
30-
willExportCallback: willExportCallback)
31-
worker.start()
32-
}
24+
public static var name: String {
25+
String(describing: Self.self)
26+
}
27+
28+
public init(
29+
spanExporter: SpanExporter,
30+
meterProvider: StableMeterProvider,
31+
scheduleDelay: TimeInterval = 5,
32+
exportTimeout: TimeInterval = 30,
33+
maxQueueSize: Int = 2048,
34+
maxExportBatchSize: Int = 512,
35+
willExportCallback: ((inout [SpanData]) -> Void)? = nil
36+
) {
37+
worker = BatchWorker(
38+
spanExporter: spanExporter,
39+
meterProvider: meterProvider,
40+
scheduleDelay: scheduleDelay,
41+
exportTimeout: exportTimeout,
42+
maxQueueSize: maxQueueSize,
43+
maxExportBatchSize: maxExportBatchSize,
44+
willExportCallback: willExportCallback
45+
)
46+
worker.start()
47+
}
3348

3449
public let isStartRequired = false
3550
public let isEndRequired = true
@@ -57,40 +72,105 @@ public struct BatchSpanProcessor: SpanProcessor {
5772
/// the data.
5873
/// The list of batched data is protected by a NSCondition which ensures full concurrency.
5974
private class BatchWorker: Thread {
60-
let spanExporter: SpanExporter
61-
let scheduleDelay: TimeInterval
62-
let maxQueueSize: Int
63-
let exportTimeout: TimeInterval
64-
let maxExportBatchSize: Int
65-
let willExportCallback: ((inout [SpanData]) -> Void)?
66-
let halfMaxQueueSize: Int
67-
private let cond = NSCondition()
68-
var spanList = [ReadableSpan]()
69-
var queue: OperationQueue
70-
71-
init(spanExporter: SpanExporter, scheduleDelay: TimeInterval, exportTimeout: TimeInterval, maxQueueSize: Int, maxExportBatchSize: Int, willExportCallback: ((inout [SpanData]) -> Void)?) {
72-
self.spanExporter = spanExporter
73-
self.scheduleDelay = scheduleDelay
74-
self.exportTimeout = exportTimeout
75-
self.maxQueueSize = maxQueueSize
76-
halfMaxQueueSize = maxQueueSize >> 1
77-
self.maxExportBatchSize = maxExportBatchSize
78-
self.willExportCallback = willExportCallback
79-
queue = OperationQueue()
80-
queue.name = "BatchWorker Queue"
81-
queue.maxConcurrentOperationCount = 1
82-
}
75+
let spanExporter: SpanExporter
76+
let meterProvider: StableMeterProvider
77+
let scheduleDelay: TimeInterval
78+
let maxQueueSize: Int
79+
let exportTimeout: TimeInterval
80+
let maxExportBatchSize: Int
81+
let willExportCallback: ((inout [SpanData]) -> Void)?
82+
let halfMaxQueueSize: Int
83+
private let cond = NSCondition()
84+
var spanList = [ReadableSpan]()
85+
var queue: OperationQueue
86+
87+
private var queueSizeGauge: ObservableLongGauge?
88+
private var spanGaugeObserver: ObservableLongGauge?
89+
90+
private var processedSpansCounter: LongCounter?
91+
private let droppedAttrs: [String: AttributeValue]
92+
private let exportedAttrs: [String: AttributeValue]
93+
private let spanGaugeBuilder: LongGaugeBuilder
94+
init(
95+
spanExporter: SpanExporter,
96+
meterProvider: StableMeterProvider,
97+
scheduleDelay: TimeInterval,
98+
exportTimeout: TimeInterval,
99+
maxQueueSize: Int,
100+
maxExportBatchSize: Int,
101+
willExportCallback: ((inout [SpanData]) -> Void)?
102+
) {
103+
self.spanExporter = spanExporter
104+
self.meterProvider = meterProvider
105+
self.scheduleDelay = scheduleDelay
106+
self.exportTimeout = exportTimeout
107+
self.maxQueueSize = maxQueueSize
108+
halfMaxQueueSize = maxQueueSize >> 1
109+
self.maxExportBatchSize = maxExportBatchSize
110+
self.willExportCallback = willExportCallback
111+
queue = OperationQueue()
112+
queue.name = "BatchWorker Queue"
113+
queue.maxConcurrentOperationCount = 1
114+
115+
let meter = meterProvider.meterBuilder(name: "io.opentelemetry.sdk.trace").build()
116+
117+
var longGaugeSdk = meter.gaugeBuilder(name: "queueSize").ofLongs() as? LongGaugeBuilderSdk
118+
longGaugeSdk = longGaugeSdk?.setDescription("The number of items queued")
119+
longGaugeSdk = longGaugeSdk?.setUnit("1")
120+
self.queueSizeGauge = longGaugeSdk?.buildWithCallback { result in
121+
result.record(
122+
value: maxQueueSize,
123+
attributes: [
124+
BatchSpanProcessor.SPAN_PROCESSOR_TYPE_LABEL: .string(BatchSpanProcessor.SPAN_PROCESSOR_TYPE_VALUE)
125+
]
126+
)
127+
}
128+
129+
self.spanGaugeBuilder = meter.gaugeBuilder(name: "spanSize")
130+
.ofLongs()
131+
132+
var longCounterSdk = meter.counterBuilder(name: "processedSpans") as? LongCounterMeterBuilderSdk
133+
longCounterSdk = longCounterSdk?.setUnit("1")
134+
longCounterSdk = longCounterSdk?.setDescription("The number of spans processed by the BatchSpanProcessor. [dropped=true if they were dropped due to high throughput]")
135+
processedSpansCounter = longCounterSdk?.build()
136+
137+
droppedAttrs = [
138+
BatchSpanProcessor.SPAN_PROCESSOR_TYPE_LABEL: .string(BatchSpanProcessor.SPAN_PROCESSOR_TYPE_VALUE),
139+
BatchSpanProcessor.SPAN_PROCESSOR_DROPPED_LABEL: .bool(true)
140+
]
141+
exportedAttrs = [
142+
BatchSpanProcessor.SPAN_PROCESSOR_TYPE_LABEL: .string(BatchSpanProcessor.SPAN_PROCESSOR_TYPE_VALUE),
143+
BatchSpanProcessor.SPAN_PROCESSOR_DROPPED_LABEL: .bool(false)
144+
]
145+
146+
// Subscribe to new gauge observer
147+
self.spanGaugeObserver = self.spanGaugeBuilder
148+
.buildWithCallback { [count = spanList.count] result in
149+
result.record(
150+
value: count,
151+
attributes: [
152+
BatchSpanProcessor.SPAN_PROCESSOR_TYPE_LABEL: .string(BatchSpanProcessor.SPAN_PROCESSOR_TYPE_VALUE)
153+
]
154+
)
155+
}
156+
}
83157

158+
deinit {
159+
// Cleanup all gauge observer
160+
self.queueSizeGauge?.close()
161+
self.spanGaugeObserver?.close()
162+
}
163+
84164
func addSpan(span: ReadableSpan) {
85165
cond.lock()
86166
defer { cond.unlock() }
87167

88168
if spanList.count == maxQueueSize {
89-
// TODO: Record a counter for dropped spans.
169+
processedSpansCounter?.add(value: 1, attribute: droppedAttrs)
90170
return
91171
}
92-
// TODO: Record a gauge for referenced spans.
93172
spanList.append(span)
173+
94174
// Notify the worker thread that at half of the queue is available. It will take
95175
// time anyway for the thread to wake up.
96176
if spanList.count >= halfMaxQueueSize {
@@ -148,11 +228,16 @@ private class BatchWorker: Thread {
148228
timeoutTimer.cancel()
149229
}
150230

151-
private func exportAction(spanList: [ReadableSpan], explicitTimeout: TimeInterval? = nil) {
152-
stride(from: 0, to: spanList.endIndex, by: maxExportBatchSize).forEach {
153-
var spansToExport = spanList[$0 ..< min($0 + maxExportBatchSize, spanList.count)].map { $0.toSpanData() }
154-
willExportCallback?(&spansToExport)
155-
spanExporter.export(spans: spansToExport, explicitTimeout: explicitTimeout)
231+
private func exportAction(spanList: [ReadableSpan], explicitTimeout: TimeInterval? = nil) {
232+
stride(from: 0, to: spanList.endIndex, by: maxExportBatchSize).forEach {
233+
var spansToExport = spanList[$0 ..< min($0 + maxExportBatchSize, spanList.count)].map { $0.toSpanData() }
234+
willExportCallback?(&spansToExport)
235+
let result = spanExporter.export(spans: spansToExport, explicitTimeout: explicitTimeout)
236+
if result == .success {
237+
cond.lock()
238+
processedSpansCounter?.add(value: spanList.count, attribute: exportedAttrs)
239+
cond.unlock()
240+
}
241+
}
156242
}
157-
}
158243
}

Tests/OpenTelemetrySdkTests/Trace/Export/BatchSpansProcessorTests.swift

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,22 @@ class BatchSpansProcessorTests: XCTestCase {
4444
}
4545

4646
func testStartEndRequirements() {
47-
let spansProcessor = BatchSpanProcessor(spanExporter: WaitingSpanExporter(numberToWaitFor: 0))
47+
let spansProcessor = BatchSpanProcessor(
48+
spanExporter: WaitingSpanExporter(numberToWaitFor: 0),
49+
meterProvider: DefaultStableMeterProvider.instance
50+
)
4851
XCTAssertFalse(spansProcessor.isStartRequired)
4952
XCTAssertTrue(spansProcessor.isEndRequired)
5053
}
5154

5255
func testExportDifferentSampledSpans() {
5356
let waitingSpanExporter = WaitingSpanExporter(numberToWaitFor: 2)
5457

55-
tracerSdkFactory.addSpanProcessor(BatchSpanProcessor(spanExporter: waitingSpanExporter, scheduleDelay: maxScheduleDelay))
58+
tracerSdkFactory.addSpanProcessor(BatchSpanProcessor(
59+
spanExporter: waitingSpanExporter,
60+
meterProvider: DefaultStableMeterProvider.instance,
61+
scheduleDelay: maxScheduleDelay)
62+
)
5663
let span1 = createSampledEndedSpan(spanName: spanName1)
5764
let span2 = createSampledEndedSpan(spanName: spanName2)
5865
let exported = waitingSpanExporter.waitForExport()
@@ -63,7 +70,12 @@ class BatchSpansProcessorTests: XCTestCase {
6370
func testExportMoreSpansThanTheBufferSize() {
6471
let waitingSpanExporter = WaitingSpanExporter(numberToWaitFor: 6)
6572

66-
tracerSdkFactory.addSpanProcessor(BatchSpanProcessor(spanExporter: waitingSpanExporter, scheduleDelay: maxScheduleDelay, maxQueueSize: 6, maxExportBatchSize: 2))
73+
tracerSdkFactory.addSpanProcessor(BatchSpanProcessor(
74+
spanExporter: waitingSpanExporter,
75+
meterProvider: DefaultStableMeterProvider.instance,
76+
scheduleDelay: maxScheduleDelay,
77+
maxQueueSize: 6, maxExportBatchSize: 2)
78+
)
6779

6880
let span1 = createSampledEndedSpan(spanName: spanName1)
6981
let span2 = createSampledEndedSpan(spanName: spanName1)
@@ -82,7 +94,13 @@ class BatchSpansProcessorTests: XCTestCase {
8294

8395
func testForceExport() {
8496
let waitingSpanExporter = WaitingSpanExporter(numberToWaitFor: 1)
85-
let batchSpansProcessor = BatchSpanProcessor(spanExporter: waitingSpanExporter, scheduleDelay: 10, maxQueueSize: 10000, maxExportBatchSize: 2000)
97+
let batchSpansProcessor = BatchSpanProcessor(
98+
spanExporter: waitingSpanExporter,
99+
meterProvider: DefaultStableMeterProvider.instance,
100+
scheduleDelay: 10,
101+
maxQueueSize: 10000,
102+
maxExportBatchSize: 2000
103+
)
86104
tracerSdkFactory.addSpanProcessor(batchSpansProcessor)
87105

88106
for _ in 0 ..< 100 {
@@ -96,7 +114,10 @@ class BatchSpansProcessorTests: XCTestCase {
96114
func testExportSpansToMultipleServices() {
97115
let waitingSpanExporter = WaitingSpanExporter(numberToWaitFor: 2)
98116
let waitingSpanExporter2 = WaitingSpanExporter(numberToWaitFor: 2)
99-
tracerSdkFactory.addSpanProcessor(BatchSpanProcessor(spanExporter: MultiSpanExporter(spanExporters: [waitingSpanExporter, waitingSpanExporter2]), scheduleDelay: maxScheduleDelay))
117+
tracerSdkFactory.addSpanProcessor(BatchSpanProcessor(
118+
spanExporter: MultiSpanExporter(spanExporters: [waitingSpanExporter, waitingSpanExporter2]),
119+
meterProvider: DefaultStableMeterProvider.instance,
120+
scheduleDelay: maxScheduleDelay))
100121

101122
let span1 = createSampledEndedSpan(spanName: spanName1)
102123
let span2 = createSampledEndedSpan(spanName: spanName2)
@@ -110,7 +131,13 @@ class BatchSpansProcessorTests: XCTestCase {
110131
let maxQueuedSpans = 8
111132
let waitingSpanExporter = WaitingSpanExporter(numberToWaitFor: maxQueuedSpans)
112133

113-
tracerSdkFactory.addSpanProcessor(BatchSpanProcessor(spanExporter: MultiSpanExporter(spanExporters: [waitingSpanExporter, blockingSpanExporter]), scheduleDelay: maxScheduleDelay, maxQueueSize: maxQueuedSpans, maxExportBatchSize: maxQueuedSpans / 2))
134+
tracerSdkFactory.addSpanProcessor(BatchSpanProcessor(
135+
spanExporter: MultiSpanExporter(spanExporters: [waitingSpanExporter, blockingSpanExporter]),
136+
meterProvider: DefaultStableMeterProvider.instance,
137+
scheduleDelay: maxScheduleDelay,
138+
maxQueueSize: maxQueuedSpans,
139+
maxExportBatchSize: maxQueuedSpans / 2)
140+
)
114141

115142
var spansToExport = [SpanData]()
116143
// Wait to block the worker thread in the BatchSampledSpansProcessor. This ensures that no items
@@ -162,7 +189,11 @@ class BatchSpansProcessorTests: XCTestCase {
162189
func testExportNotSampledSpans() {
163190
let waitingSpanExporter = WaitingSpanExporter(numberToWaitFor: 1)
164191

165-
tracerSdkFactory.addSpanProcessor(BatchSpanProcessor(spanExporter: waitingSpanExporter, scheduleDelay: maxScheduleDelay))
192+
tracerSdkFactory.addSpanProcessor(BatchSpanProcessor(
193+
spanExporter: waitingSpanExporter,
194+
meterProvider: DefaultStableMeterProvider.instance,
195+
scheduleDelay: maxScheduleDelay)
196+
)
166197

167198
createNotSampledEndedSpan(spanName: spanName1)
168199
createNotSampledEndedSpan(spanName: spanName2)
@@ -181,7 +212,11 @@ class BatchSpansProcessorTests: XCTestCase {
181212
let waitingSpanExporter = WaitingSpanExporter(numberToWaitFor: 1)
182213

183214
// Set the export delay to zero, for no timeout, in order to confirm the #flush() below works
184-
tracerSdkFactory.addSpanProcessor(BatchSpanProcessor(spanExporter: waitingSpanExporter, scheduleDelay: 0.1))
215+
tracerSdkFactory.addSpanProcessor(BatchSpanProcessor(
216+
spanExporter: waitingSpanExporter,
217+
meterProvider: DefaultStableMeterProvider.instance,
218+
scheduleDelay: 0.1)
219+
)
185220

186221
let span2 = createSampledEndedSpan(spanName: spanName2)
187222

0 commit comments

Comments
 (0)