-
Notifications
You must be signed in to change notification settings - Fork 0
fix: Otlp Trace Exporters to Event Exporter #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
347b603
cb3e57c
c7081e5
c208530
9a5e739
e4b34fa
4903390
c47ff1b
f509f7d
e8ccb71
eadbe12
6c7b130
787234a
5d9edd6
96e3cc8
c6bcd12
37232a9
86c1845
ec8bfca
98f1c6c
14ec46a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import Foundation | ||
| import OpenTelemetryApi | ||
| import OpenTelemetrySdk | ||
|
|
||
| final class EventSpanProcessor: SpanProcessor { | ||
| private let eventQueue: EventQueue | ||
| private let sampler: ExportSampler | ||
| let isStartRequired = false | ||
| let isEndRequired = true | ||
|
|
||
| init(eventQueue: EventQueue, sampler: ExportSampler) { | ||
| self.eventQueue = eventQueue | ||
| self.sampler = sampler | ||
| } | ||
|
|
||
| func onStart(parentContext: OpenTelemetryApi.SpanContext?, span: any OpenTelemetrySdk.ReadableSpan) { | ||
| // No-op | ||
| } | ||
|
|
||
| func onEnd(span: any OpenTelemetrySdk.ReadableSpan) { | ||
| if !span.context.traceFlags.sampled { | ||
| return | ||
| } | ||
|
|
||
| let spanData = span.toSpanData() | ||
| let sampledItems = sampler.sampleSpans(items: [spanData]) | ||
| guard !sampledItems.isEmpty else { | ||
| return | ||
| } | ||
|
|
||
| Task { | ||
| await eventQueue.send(SpanItem(spanData: spanData)) | ||
| } | ||
| } | ||
|
|
||
| func shutdown(explicitTimeout: TimeInterval?) { | ||
| // No-op | ||
| } | ||
|
|
||
| func forceFlush(timeout: TimeInterval?) { | ||
| // No-op | ||
| } | ||
|
|
||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import OpenTelemetrySdk | ||
| import Common | ||
| import Foundation | ||
| import OpenTelemetryProtocolExporterCommon | ||
|
|
||
| final class OtlpTraceEventExporter: EventExporting { | ||
| private let otlpHttpClient: OtlpHttpClient | ||
|
|
||
| init(endpoint: URL, | ||
| config: OtlpConfiguration = OtlpConfiguration(), | ||
| useSession: URLSession? = nil, | ||
| envVarHeaders: [(String, String)]? = EnvVarHeaders.attributes) { | ||
| self.otlpHttpClient = OtlpHttpClient(endpoint: endpoint, | ||
| config: config, | ||
| useSession: useSession, | ||
| envVarHeaders: envVarHeaders) | ||
| } | ||
|
|
||
| func export(items: [EventQueueItem]) async throws { | ||
| let spanDatas: [OpenTelemetrySdk.SpanData] = items.compactMap { item in | ||
| (item.payload as? SpanItem)?.spanData | ||
| } | ||
| guard spanDatas.isNotEmpty else { | ||
| return | ||
| } | ||
| try await export(spanDatas: spanDatas) | ||
| } | ||
|
|
||
| private func export(spanDatas: [OpenTelemetrySdk.SpanData], | ||
| explicitTimeout: TimeInterval? = nil) async throws { | ||
| let body = | ||
| Opentelemetry_Proto_Collector_Trace_V1_ExportTraceServiceRequest.with { | ||
| $0.resourceSpans = SpanAdapter.toProtoResourceSpans( | ||
| spanDataList: spanDatas) | ||
| } | ||
|
|
||
| try await otlpHttpClient.send(body: body, explicitTimeout: explicitTimeout) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import Foundation | ||
| import OpenTelemetrySdk | ||
|
|
||
| struct SpanItem: EventQueueItemPayload { | ||
| var exporterClass: AnyClass { | ||
| Observability.OtlpTraceEventExporter.self | ||
| } | ||
|
|
||
| let spanData: SpanData | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. private ? understanding that all coming from EventQueueItemPayload could be internal, but why spanData is internal
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need read spanData in exporter. SpanData is what Protobuf exporter uses |
||
| var timestamp: TimeInterval | ||
|
|
||
| init(spanData: SpanData) { | ||
| self.spanData = spanData | ||
| self.timestamp = spanData.endTime.timeIntervalSince1970 | ||
| } | ||
|
|
||
| func cost() -> Int { | ||
| 300 + spanData.events.count * 100 + spanData.attributes.count * 100 | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,30 +5,18 @@ final class TracerDecorator: Tracer { | |
| private let options: Options | ||
| private let sessionManager: SessionManaging | ||
| private let tracerProvider: any TracerProvider | ||
| private let spanProcessor: any SpanProcessor | ||
| private let tracer: any Tracer | ||
|
|
||
| init(options: Options, sessionManager: SessionManaging, exporter: SpanExporter) { | ||
| private var activeSpan: Span? | ||
|
|
||
| init(options: Options, sessionManager: SessionManaging, sampler: ExportSampler, eventQueue: EventQueue) { | ||
| self.options = options | ||
| self.sessionManager = sessionManager | ||
| /// Using the default values from OpenTelemetry for Swift | ||
| /// For reference check: | ||
| ///https://github.com/open-telemetry/opentelemetry-swift/blob/main/Sources/OpenTelemetrySdk/Trace/SpanProcessors/BatchSpanProcessor.swift | ||
| let processor = BatchSpanProcessor( | ||
| spanExporter: exporter, | ||
| scheduleDelay: 5, | ||
| exportTimeout: 30, | ||
| maxQueueSize: 2048, | ||
| maxExportBatchSize: 512, | ||
| ) | ||
| self.spanProcessor = processor | ||
| let processor = EventSpanProcessor(eventQueue: eventQueue, sampler: sampler) | ||
| let provider = TracerProviderBuilder() | ||
| .add(spanProcessor: processor) | ||
| .with(resource: Resource(attributes: options.resourceAttributes)) | ||
| .build() | ||
| self.tracerProvider = provider | ||
|
|
||
|
|
||
| self.tracer = tracerProvider.get( | ||
| instrumentationName: options.serviceName, | ||
| instrumentationVersion: options.serviceVersion, | ||
|
|
@@ -84,20 +72,14 @@ extension TracerDecorator: TracesApi { | |
| } | ||
|
|
||
| func flush() -> Bool { | ||
| /// span processor flush method differs from metrics and logs, it doesn't return a Result type | ||
| /// Processes all span events that have not yet been processed. | ||
| /// This method is executed synchronously on the calling thread | ||
| /// - Parameter timeout: Maximum time the flush complete or abort. If nil, it will wait indefinitely | ||
| /// func forceFlush(timeout: TimeInterval?) | ||
| spanProcessor.forceFlush(timeout: 5.0) | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| /// Internal method used to set span start date | ||
| extension TracerDecorator { | ||
| func startSpan(name: String, attributes: [String : AttributeValue], startTime: Date = Date()) -> any Span { | ||
| let builder = tracer.spanBuilder(spanName: name) | ||
| extension Tracer { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Flush() Fails to Export Pending SpansThe flush() method now returns true without actually flushing any data. The previous implementation called spanProcessor.forceFlush(timeout: 5.0) to ensure spans were exported. With the new event queue architecture, the flush() method should trigger flushing of the eventQueue or batchWorker to ensure pending spans are exported, but it currently does nothing. This could lead to data loss if callers rely on flush() to ensure spans are exported before shutdown or other critical operations.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is just server requirement from protocol and doesn't needed for mobile |
||
| func startSpan(name: String, attributes: [String : AttributeValue], startTime: Date) -> any Span { | ||
| let builder = spanBuilder(spanName: name) | ||
|
|
||
| if let parent = OpenTelemetry.instance.contextProvider.activeSpan { | ||
| builder.setParent(parent) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Asynchronous Exporter Registration Causes Lost Traces Metrics Logs
Race condition: The trace exporter is added to batchWorker in a fire-and-forget Task, but execution continues immediately without waiting. This means traces could be generated and sent to the eventQueue before the exporter is registered with batchWorker, causing those traces to be lost or not exported properly. The same issue exists for metrics (lines 123-125) and logs (lines 54-56). The Task should be awaited or the exporter should be added synchronously before the TracerDecorator is created and used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All good. It will be just in buffer for this time.