Skip to content

Commit d4b6820

Browse files
feat: plugin implementation
1 parent 647e2b4 commit d4b6820

File tree

15 files changed

+764
-241
lines changed

15 files changed

+764
-241
lines changed

Package.resolved

Lines changed: 30 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,20 @@ let package = Package(
77
products: [
88
.library(
99
name: "LaunchDarklyObservability",
10-
targets: ["LaunchDarklyObservability"]),
10+
targets: ["Client", "LaunchDarklyObservability", "ObservabilityPlugins"]),
1111
],
1212
dependencies: [
13-
.package(url: "https://github.com/open-telemetry/opentelemetry-swift", from: "1.0.0"),
13+
.package(url: "https://github.com/open-telemetry/opentelemetry-swift", from: "2.0.0"),
14+
.package(url: "https://github.com/launchdarkly/ios-client-sdk.git", from: "9.15.0"),
1415
],
1516
targets: [
17+
.target(
18+
name: "ObserveAPI",
19+
dependencies: [
20+
.product(name: "OpenTelemetryApi", package: "opentelemetry-swift"),
21+
.product(name: "OpenTelemetrySdk", package: "opentelemetry-swift"),
22+
]
23+
),
1624
.target(
1725
name: "Instrumentation",
1826
dependencies: [
@@ -29,17 +37,38 @@ let package = Package(
2937
.target(
3038
name: "LaunchDarklyObservability",
3139
dependencies: [
32-
"Instrumentation",
40+
"ObserveAPI",
41+
"Client",
3342
.product(name: "OpenTelemetryApi", package: "opentelemetry-swift"),
3443
.product(name: "OpenTelemetrySdk", package: "opentelemetry-swift"),
35-
.product(name: "OpenTelemetryProtocolExporterHTTP", package: "opentelemetry-swift"),
36-
.product(name: "StdoutExporter", package: "opentelemetry-swift"),
37-
.product(name: "ResourceExtension", package: "opentelemetry-swift"),
3844
]
3945
),
4046
.testTarget(
4147
name: "LaunchDarklyObservabilityTests",
4248
dependencies: ["LaunchDarklyObservability"]
4349
),
50+
.target(
51+
name: "Client",
52+
dependencies: [
53+
"ObserveAPI",
54+
"Instrumentation",
55+
.product(name: "OpenTelemetryApi", package: "opentelemetry-swift"),
56+
.product(name: "OpenTelemetrySdk", package: "opentelemetry-swift"),
57+
.product(name: "OpenTelemetryProtocolExporterHTTP", package: "opentelemetry-swift"),
58+
.product(name: "StdoutExporter", package: "opentelemetry-swift"),
59+
.product(name: "ResourceExtension", package: "opentelemetry-swift"),
60+
]
61+
),
62+
.target(
63+
name: "ObservabilityPlugins",
64+
dependencies: [
65+
"Client",
66+
"LaunchDarklyObservability",
67+
"Instrumentation",
68+
.product(name: "ResourceExtension", package: "opentelemetry-swift"),
69+
.product(name: "OpenTelemetryApi", package: "opentelemetry-swift"),
70+
.product(name: "LaunchDarkly", package: "ios-client-sdk"),
71+
]
72+
)
4473
]
4574
)
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import Foundation.NSDate
2+
import UIKit.UIApplication
3+
import ObserveAPI
4+
import OpenTelemetryApi
5+
import OpenTelemetrySdk
6+
import ResourceExtension
7+
@_exported import Instrumentation
8+
9+
import Combine
10+
11+
public final class ObservabilityClient: @unchecked Sendable, Observe {
12+
private let lock = NSLock()
13+
private let tracerFacade: TracerFacade
14+
private let loggerFacade: LoggerFacade
15+
private let meterFacade: MeterFacade
16+
private var session: Session
17+
18+
private var cachedGauges = [String: DoubleGauge]()
19+
private var cachedCounters = [String: DoubleCounter]()
20+
private var cachedLongCounters = [String: LongCounter]()
21+
private var cachedHistograms = [String: DoubleHistogram]()
22+
private var cachedUpDownCounters = [String: DoubleUpDownCounter]()
23+
24+
private var cachedSpans = [String: Span]()
25+
private var cancellables = Set<AnyCancellable>()
26+
27+
private var onWillEndSession: @Sendable (_ sessionId: String) -> Void {
28+
{ [weak self] sessionId in
29+
self?.willEndSession(sessionId)
30+
}
31+
}
32+
private var onDidStartSession: @Sendable (_ sessionId: String) -> Void {
33+
{ [weak self] sessionId in
34+
self?.didStartSession(sessionId)
35+
}
36+
}
37+
38+
private var onWillTerminate: @Sendable () -> Void {
39+
{ [weak self] in
40+
self?.tracerFacade.shutdown()
41+
}
42+
}
43+
44+
deinit {
45+
// If observability client is used through the LDObserve wrapper, this never will be call
46+
// since the LDObserve wrapper is a singleton so, at some point we will need to call it manually
47+
self.tracerFacade.shutdown()
48+
}
49+
50+
public init(configuration: Configuration = .init(), sdkKey: String = "") {
51+
self.tracerFacade = TracerFacade(configuration: configuration)
52+
self.loggerFacade = LoggerFacade(configuration: configuration)
53+
self.meterFacade = MeterFacade(configuration: configuration)
54+
self.session = Session(options: SessionOptions(timeout: configuration.sessionTimeout))
55+
self.registerPropagators()
56+
57+
58+
self.session.start(
59+
onWillEndSession: onWillEndSession,
60+
onDidStartSession: onDidStartSession
61+
)
62+
self.session.onWillTerminate(onWillTerminate)
63+
}
64+
65+
private func didStartSession(_ id: String) {
66+
let span = spanBuilder(spanName: "app.session.started")
67+
.setSpanKind(spanKind: .client)
68+
.startSpan()
69+
cachedSpans[id] = span
70+
}
71+
72+
private func willEndSession(_ id: String) {
73+
guard let span = cachedSpans[id] else { return }
74+
span.end()
75+
}
76+
77+
private func registerPropagators() {
78+
OpenTelemetry.registerPropagators(
79+
textPropagators: [
80+
W3CTraceContextPropagator(),
81+
B3Propagator(),
82+
JaegerPropagator(),
83+
],
84+
baggagePropagator: W3CBaggagePropagator()
85+
)
86+
}
87+
88+
// MARK: - Public API
89+
90+
public static func defaultResource() -> Resource {
91+
DefaultResources().get()
92+
}
93+
94+
public func spanBuilder(spanName: String) -> SpanBuilder {
95+
tracerFacade.spanBuilder(spanName: spanName)
96+
}
97+
98+
99+
public func spanBuilder(spanName: String, attributes: [String: AttributeValue]) -> Span {
100+
let builder = tracerFacade
101+
.spanBuilder(spanName: spanName)
102+
103+
attributes.forEach {
104+
builder.setAttribute(key: $0.key, value: $0.value)
105+
}
106+
107+
return builder.startSpan()
108+
}
109+
110+
111+
public func recordMetric(metric: ObserveAPI.Metric) {
112+
lock.lock()
113+
defer { lock.unlock() }
114+
var gauge = cachedGauges[metric.name]
115+
if gauge == nil {
116+
gauge = meterFacade.meter
117+
.gaugeBuilder(name: metric.name)
118+
.build()
119+
cachedGauges[metric.name] = gauge
120+
}
121+
gauge?.record(value: metric.value, attributes: metric.attributes)
122+
}
123+
124+
public func recordCount(metric: ObserveAPI.Metric) {
125+
lock.lock()
126+
defer { lock.unlock() }
127+
var counter = cachedCounters[metric.name]
128+
if counter == nil {
129+
counter = meterFacade.meter.counterBuilder(name: metric.name).ofDoubles().build()
130+
cachedCounters[metric.name] = counter
131+
}
132+
counter?.add(value: metric.value, attributes: metric.attributes)
133+
}
134+
135+
public func recordIncr(metric: ObserveAPI.Metric) {
136+
lock.lock()
137+
defer { lock.unlock() }
138+
var counter = cachedLongCounters[metric.name]
139+
if counter == nil {
140+
counter = meterFacade.meter.counterBuilder(name: metric.name).build()
141+
cachedLongCounters[metric.name] = counter
142+
}
143+
counter?.add(value: 1, attributes: metric.attributes)
144+
}
145+
146+
public func recordHistogram(metric: ObserveAPI.Metric) {
147+
lock.lock()
148+
defer { lock.unlock() }
149+
var histogram = cachedHistograms[metric.name]
150+
if histogram == nil {
151+
histogram = meterFacade.meter.histogramBuilder(name: metric.name).build()
152+
cachedHistograms[metric.name] = histogram
153+
}
154+
histogram?.record(value: metric.value, attributes: metric.attributes)
155+
}
156+
157+
public func recordUpDownCounter(metric: ObserveAPI.Metric) {
158+
lock.lock()
159+
defer { lock.unlock() }
160+
var upDownCounter = cachedUpDownCounters[metric.name]
161+
if upDownCounter == nil {
162+
upDownCounter = meterFacade.meter.upDownCounterBuilder(name: metric.name).ofDoubles().build()
163+
cachedUpDownCounters[metric.name] = upDownCounter
164+
}
165+
upDownCounter?.add(value: metric.value, attributes: metric.attributes)
166+
}
167+
168+
public func recordError(error: any Error, attributes: [String : OpenTelemetryApi.AttributeValue]) {
169+
lock.lock()
170+
defer { lock.unlock() }
171+
let builder = tracerFacade.tracer.spanBuilder(spanName: "highlight.error")
172+
173+
if let parent = tracerFacade.currentSpan {
174+
builder.setParent(parent)
175+
}
176+
177+
attributes.forEach {
178+
builder.setAttribute(key: $0.key, value: $0.value)
179+
}
180+
181+
let span = builder.startSpan()
182+
span.setAttributes(attributes)
183+
span.recordException(ErrorSpanException(error: error), attributes: attributes)
184+
span.end()
185+
}
186+
187+
public func recordLog(message: String, severity: Severity, attributes: [String : OpenTelemetryApi.AttributeValue]) {
188+
lock.lock()
189+
defer { lock.unlock() }
190+
loggerFacade.logger.logRecordBuilder()
191+
.setBody(.string(message))
192+
.setTimestamp(.now)
193+
.setSeverity(severity)
194+
.setAttributes(attributes)
195+
.emit()
196+
}
197+
198+
public func startSpan(name: String, attributes: [String : OpenTelemetryApi.AttributeValue]) -> any OpenTelemetryApi.Span {
199+
lock.lock()
200+
defer { lock.unlock() }
201+
let builder = tracerFacade
202+
.spanBuilder(spanName: name)
203+
204+
if let parent = tracerFacade.currentSpan {
205+
builder.setParent(parent)
206+
}
207+
208+
attributes.forEach {
209+
builder.setAttribute(key: $0.key, value: $0.value)
210+
}
211+
212+
return builder.startSpan()
213+
}
214+
215+
public func flush() {
216+
lock.lock()
217+
defer { lock.unlock() }
218+
tracerFacade.flush()
219+
}
220+
}
221+
222+
struct ErrorSpanException: SpanException {
223+
private let error: Error
224+
var type: String {
225+
String(describing: error)
226+
}
227+
228+
var message: String? {
229+
String(describing: error)
230+
}
231+
232+
var stackTrace: [String]? {
233+
Thread.callStackSymbols
234+
}
235+
236+
init(error: Error) {
237+
self.error = error
238+
}
239+
}

0 commit comments

Comments
 (0)