Skip to content

Commit b792a5c

Browse files
refactor: observability sdk (#25)
<!-- CURSOR_SUMMARY --> > [!NOTE] > Rearchitects the SDK into modular services (logs/traces/metrics/session/crash) using OpenTelemetry exporters and new domain models, adds sampling and URLSession instrumentation, and replaces legacy modules and wiring. > > - **Architecture**: > - Split into `DomainModels`, `ApplicationServices`, `OTelInstrumentation`, `ObservabilityServiceLive`, `iOSSessionService`, and `KSCrashReportService` targets; remove legacy `Observability`, `Instrumentation`, and `API/Observe` layers. > - Reorganize `Package.swift` to reflect new modules and tests. > - **Core APIs**: > - Introduce domain types: `AttributeValue`, `Severity`, `Span`, `Metric`, and updated `SemanticConvention`. > - Add service facades: `ObservabilityService` (aggregates `MetricsService`, `TracesService`, `LogsService`), `SessionService`, `UserInteractionService`, `CrashReportService` with async `flush`. > - Redesign `Options` (feature flags for `logs`/`traces`/`metrics`, `tracingOrigins`, `urlBlocklist`, `customHeaders` as dict, `OSLog`). > - Update `LDObserve` to use `ObservabilityService` with concurrent access and async `flush`. > - **OTEL Instrumentation**: > - Implement HTTP exporters for logs/metrics/traces, sampling decorators, and resource propagation; add `URLSession` instrumentation and span customization. > - Add user interaction spans (`user.tap`, `user.swipe`). > - **Crash Reporting**: > - Replace old crash reporter with `KSCrashReportService` and `LDCrashFilter` that emits fatal logs via `LogsService` and awaits async flush. > - **Sampling**: > - Refactor sampler into `SamplingLive` (`ExportSampler.build/customSampler`), add helpers to sample logs/spans and propagate `launchdarkly.sampling.ratio` attribute. > - **LaunchDarkly integration**: > - `Observability` plugin builds `ObservabilityService`, enriches resource and headers with `launchdarkly.sdk.version` and `highlight.project_id`. > - `EvalTracingHook` now starts/ends spans via `LDObserve`. > - **Example App/Views**: > - Adjust imports; `TraceView` ends span and triggers `LDObserve.shared.flush()`; config uses new `Options` flags. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d11f1b9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 12e35c7 commit b792a5c

File tree

77 files changed

+1908
-1675
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+1908
-1675
lines changed

ExampleApp/ExampleApp/AppDelegate.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,19 @@ let config = { () -> LDConfig in
88
mobileKey: mobileKey,
99
autoEnvAttributes: .enabled
1010
)
11+
config.plugins = [
12+
Observability(
13+
options: .init(
14+
otlpEndpoint: "http://localhost:4318",
15+
sessionBackgroundTimeout: 3,
16+
isDebug: true,
17+
logs: .enabled,
18+
traces: .enabled,
19+
metrics: .enabled
20+
)
21+
)
22+
]
23+
/*
1124
config.plugins = [
1225
Observability(
1326
options: .init(
@@ -20,6 +33,7 @@ let config = { () -> LDConfig in
2033
)
2134
)
2235
]
36+
*/
2337
return config
2438
}()
2539

ExampleApp/ExampleApp/Instrumentation/Manual/InstrumentationView.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import SwiftUI
22
import LaunchDarklyObservability
3-
import OpenTelemetryApi
43

54
struct InstrumentationView: View {
65
var body: some View {

ExampleApp/ExampleApp/Instrumentation/Manual/LogsView.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import SwiftUI
22
import LaunchDarklyObservability
3-
import OpenTelemetryApi
43

54

65
struct LogsView: View {

ExampleApp/ExampleApp/Instrumentation/Manual/TraceView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import SwiftUI
22
import LaunchDarklyObservability
3-
import OpenTelemetryApi
43

54

65
struct TraceView: View {
@@ -30,6 +29,7 @@ struct TraceView: View {
3029
.task(id: started) {
3130
guard started else {
3231
span?.end()
32+
await LDObserve.shared.flush()
3333
return name = ""
3434
}
3535
span = LDObserve.shared.startSpan(name: name)

Package.swift

Lines changed: 65 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,63 @@ let package = Package(
1515
.package(url: "https://github.com/kstenerud/KSCrash.git", from: "2.3.0"),
1616
],
1717
targets: [
18-
.target(name: "Common"),
1918
.target(
20-
name: "API",
19+
name: "DomainModels"
20+
),
21+
.target(
22+
name: "DomainServices",
2123
dependencies: [
22-
.product(name: "OpenTelemetrySdk", package: "opentelemetry-swift"),
23-
.product(name: "OpenTelemetryApi", package: "opentelemetry-swift"),
24-
.product(name: "ResourceExtension", package: "opentelemetry-swift"),
24+
"DomainModels"
2525
]
2626
),
27-
.testTarget(
28-
name: "CommonTests",
27+
.target(
28+
name: "ApplicationServices",
2929
dependencies: [
30-
"Common"
31-
],
32-
resources: [.process("GraphQL/Queries")]
30+
"DomainModels",
31+
"DomainServices"
32+
]
3333
),
34-
.target(name: "CrashReporter"),
3534
.target(
36-
name: "CrashReporterLive",
35+
name: "iOSSessionService",
36+
dependencies: [
37+
"DomainModels",
38+
"DomainServices",
39+
"ApplicationServices"
40+
]
41+
),
42+
.target(
43+
name: "KSCrashReportService",
44+
dependencies: [
45+
"DomainModels",
46+
"DomainServices",
47+
"ApplicationServices",
48+
.product(name: "Installations", package: "KSCrash")
49+
]
50+
),
51+
.target(
52+
name: "OTelInstrumentation",
3753
dependencies: [
38-
"CrashReporter",
3954
"Common",
55+
"DomainModels",
56+
"DomainServices",
57+
"ApplicationServices",
58+
"Sampling",
59+
.product(name: "OpenTelemetrySdk", package: "opentelemetry-swift"),
4060
.product(name: "OpenTelemetryApi", package: "opentelemetry-swift"),
61+
.product(name: "URLSessionInstrumentation", package: "opentelemetry-swift"),
62+
.product(name: "ResourceExtension", package: "opentelemetry-swift"),
63+
.product(name: "OpenTelemetryProtocolExporterHTTP", package: "opentelemetry-swift"),
64+
.product(name: "InMemoryExporter", package: "opentelemetry-swift"),
65+
.product(name: "OTelSwiftLog", package: "opentelemetry-swift"),
66+
]
67+
),
68+
.testTarget(
69+
name: "OTelInstrumentationServiceTests",
70+
dependencies: [
71+
"OTelInstrumentation",
4172
.product(name: "OpenTelemetrySdk", package: "opentelemetry-swift"),
42-
.product(name: "Installations", package: "KSCrash")
73+
.product(name: "OpenTelemetryApi", package: "opentelemetry-swift"),
74+
.product(name: "OpenTelemetryProtocolExporterHTTP", package: "opentelemetry-swift"),
4375
]
4476
),
4577
.target(
@@ -52,6 +84,7 @@ let package = Package(
5284
.target(
5385
name: "SamplingLive",
5486
dependencies: [
87+
"DomainModels",
5588
"Sampling",
5689
"Common",
5790
.product(name: "OpenTelemetryApi", package: "opentelemetry-swift"),
@@ -61,6 +94,7 @@ let package = Package(
6194
.testTarget(
6295
name: "SamplingLiveTests",
6396
dependencies: [
97+
"DomainModels",
6498
"Sampling",
6599
"SamplingLive",
66100
"Common",
@@ -73,43 +107,34 @@ let package = Package(
73107
]
74108
),
75109
.target(
76-
name: "Instrumentation",
110+
name: "ObservabilityServiceLive",
77111
dependencies: [
78-
"API",
79-
"CrashReporter",
80-
"Sampling",
81-
.product(name: "OpenTelemetrySdk", package: "opentelemetry-swift"),
82-
.product(name: "OpenTelemetryApi", package: "opentelemetry-swift"),
83-
]
84-
),
85-
.target(
86-
name: "Observability",
87-
dependencies: [
88-
"Common",
89-
"API",
90-
"CrashReporter",
91-
"CrashReporterLive",
112+
"ApplicationServices",
113+
"OTelInstrumentation",
114+
"KSCrashReportService",
115+
"iOSSessionService",
92116
"Sampling",
93117
"SamplingLive",
94-
"Instrumentation",
95-
.product(name: "OpenTelemetrySdk", package: "opentelemetry-swift"),
96-
.product(name: "OpenTelemetryApi", package: "opentelemetry-swift"),
97-
.product(name: "OpenTelemetryProtocolExporterHTTP", package: "opentelemetry-swift"),
98-
.product(name: "URLSessionInstrumentation", package: "opentelemetry-swift"),
118+
"Common"
99119
],
100120
resources: [
101121
.process("Resources"),
102122
]
103123
),
124+
.target(name: "Common"),
125+
.testTarget(
126+
name: "CommonTests",
127+
dependencies: [
128+
"Common"
129+
],
130+
resources: [.process("GraphQL/Queries")]
131+
),
104132
.target(
105133
name: "LaunchDarklyObservability",
106134
dependencies: [
107-
"Observability",
108-
"API",
109-
"Common",
110-
.product(name: "LaunchDarkly", package: "ios-client-sdk"),
111-
.product(name: "OpenTelemetrySdk", package: "opentelemetry-swift"),
112-
.product(name: "OpenTelemetryApi", package: "opentelemetry-swift"),
135+
"ApplicationServices",
136+
"ObservabilityServiceLive",
137+
.product(name: "LaunchDarkly", package: "ios-client-sdk")
113138
]
114139
)
115140
]

Sources/API/Observe.swift

Lines changed: 0 additions & 58 deletions
This file was deleted.
Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
public struct CrashReporter {
2-
public var install: () throws -> Void
1+
public struct CrashReportService {
32
public var logPendingCrashReports: () -> Void
43

54
public init(
6-
install: @escaping () throws -> Void,
75
logPendingCrashReports: @escaping () -> Void
86
) {
9-
self.install = install
107
self.logPendingCrashReports = logPendingCrashReports
118
}
129
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
public enum InstrumentationError: Error {
2+
case invalidTraceExporterUrl
3+
case invalidLogExporterUrl
4+
case invalidMetricExporterUrl
5+
case invalidGraphQLUrl
6+
case unableToLoadReportStore
7+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@_exported import DomainModels
2+
3+
public struct LogsService {
4+
public var recordLog: (_ message: String, _ severity: Severity, _ attributes: [String: AttributeValue]) -> Void
5+
public var flush: () async -> Bool
6+
7+
public init(
8+
recordLog: @escaping (_: String, _: Severity, _: [String : AttributeValue]) -> Void,
9+
flush: @escaping () async -> Bool
10+
) {
11+
self.recordLog = recordLog
12+
self.flush = flush
13+
}
14+
15+
public func recordLog(message: String, severity: Severity, attributes: [String: AttributeValue]) {
16+
recordLog(message, severity, attributes)
17+
}
18+
}
Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,26 @@
1-
import OpenTelemetryApi
1+
@_exported import DomainModels
22

3-
import API
4-
import Sampling
5-
6-
public struct Instrumentation {
3+
public struct MetricsService {
74
public var recordMetric: (_ metric: Metric) -> Void
85
public var recordCount: (_ metric: Metric) -> Void
96
public var recordIncr: (_ metric: Metric) -> Void
107
public var recordHistogram: (_ metric: Metric) -> Void
118
public var recordUpDownCounter: (_ metric: Metric) -> Void
12-
public var recordError: (_ error: Error, _ attributes: [String: AttributeValue]) -> Void
13-
public var recordLog: (_ message: String, _ severity: Severity, _ attributes: [String: AttributeValue]) -> Void
14-
public var startSpan: (_ name: String, _ attributes: [String: AttributeValue]) -> Span
15-
public var flush: () -> Bool
9+
public var flush: () async -> Bool
1610

1711
public init(
1812
recordMetric: @escaping (_: Metric) -> Void,
1913
recordCount: @escaping (_: Metric) -> Void,
2014
recordIncr: @escaping (_: Metric) -> Void,
2115
recordHistogram: @escaping (_: Metric) -> Void,
2216
recordUpDownCounter: @escaping (_: Metric) -> Void,
23-
recordError: @escaping (_: Error, _: [String : AttributeValue]) -> Void,
24-
recordLog: @escaping (_: String, _: Severity, _: [String : AttributeValue]) -> Void,
25-
startSpan: @escaping (_: String, _: [String : AttributeValue]) -> Span,
26-
flush: @escaping () -> Bool
17+
flush: @escaping () async -> Bool
2718
) {
2819
self.recordMetric = recordMetric
2920
self.recordCount = recordCount
3021
self.recordIncr = recordIncr
3122
self.recordHistogram = recordHistogram
3223
self.recordUpDownCounter = recordUpDownCounter
33-
self.recordError = recordError
34-
self.recordLog = recordLog
35-
self.startSpan = startSpan
3624
self.flush = flush
3725
}
3826

@@ -55,16 +43,4 @@ public struct Instrumentation {
5543
public func recordUpDownCounter(metric: Metric) {
5644
recordUpDownCounter(metric)
5745
}
58-
59-
public func recordError(error: Error, attributes: [String: AttributeValue]) {
60-
recordError(error, attributes)
61-
}
62-
63-
public func recordLog(message: String, severity: Severity, attributes: [String: AttributeValue]) {
64-
recordLog(message, severity, attributes)
65-
}
66-
67-
public func startSpan(name: String, attributes: [String: AttributeValue]) -> Span {
68-
startSpan(name, attributes)
69-
}
7046
}

0 commit comments

Comments
 (0)