Skip to content

Commit ada7cdf

Browse files
feat: App life cycle logging (#48)
App life cycle logging <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds app lifecycle auto-logging and refactors log building/queuing via AppLogBuilder and LDLogRecordBuilder; removes flush APIs and updates memory/crash handling. > > - **Auto-instrumentation**: > - **App lifecycle logging**: New `AppLifecycleLogger` emits `device.app.lifecycle` logs; integrated in `ObservabilityClientFactory`. > - **Memory warnings**: `MemoryPressureMonitor` now builds logs with `AppLogBuilder` and yields `ReadableLogRecord` to queue. > - **Logging pipeline**: > - Introduce `AppLogBuilder` to construct logs (adds session id, scope, resource) and `LoggerDecorator` now enqueues built `ReadableLogRecord`. > - `LDLogRecordBuilder` decoupled from queue; returns `ReadableLogRecord` via `readableLogRecord()` and removes `emit()`. > - **API changes**: > - Remove `flush()` from `Observe`, `LogsApi`, `TracesApi` and corresponding implementations in `LDObserve`, `ObservabilityClient`, and usage in `LDCrashFilter`. > - **Crash reporting**: > - `LDCrashFilter` now completes without forcing a flush after logging. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6eb1749. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 65215ef commit ada7cdf

File tree

9 files changed

+178
-103
lines changed

9 files changed

+178
-103
lines changed

Sources/LaunchDarklyObservability/LDObserve.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,6 @@ extension LDObserve: Observe {
4646
client.recordUpDownCounter(metric: metric)
4747
}
4848

49-
public func flush() -> Bool {
50-
client.flush()
51-
}
52-
5349
public func recordLog(message: String, severity: Severity, attributes: [String : AttributeValue]) {
5450
client.recordLog(message: message, severity: severity, attributes: attributes)
5551
}

Sources/Observability/Api/Observe.swift

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ public protocol MetricsApi {
2525
/// Record an up/down counter metric.
2626
/// - metric The up/down counter metric to record
2727
func recordUpDownCounter(metric: Metric)
28-
/// Flushes all pending telemetry data (traces, logs, metrics).
29-
/// - true if all flush operations succeeded, false otherwise
30-
func flush() -> Bool
3128
}
3229

3330
public protocol LogsApi {
@@ -36,9 +33,6 @@ public protocol LogsApi {
3633
/// - severity The severity of the log message
3734
/// - attributes The attributes to record with the log message
3835
func recordLog(message: String, severity: Severity, attributes: [String : AttributeValue])
39-
/// Flushes all pending telemetry data (traces, logs, metrics).
40-
/// - true if all flush operations succeeded, false otherwise
41-
func flush() -> Bool
4236
}
4337

4438
public protocol TracesApi {
@@ -50,7 +44,4 @@ public protocol TracesApi {
5044
/// - name The name of the span
5145
/// - attributes The attributes to record with the span
5246
func startSpan(name: String, attributes: [String : AttributeValue]) -> Span
53-
/// Flushes all pending telemetry data (traces, logs, metrics).
54-
/// - true if all flush operations succeeded, false otherwise
55-
func flush() -> Bool
5647
}

Sources/Observability/AutoInstrumentation/Memory/MemoryPressureMonitor.swift

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import Foundation
2+
import OpenTelemetrySdk
23

34
final class MemoryPressureMonitor: AutoInstrumentation {
45
private let options: Options
5-
private let logsApi: LogsApi
6+
private let appLogBuilder: AppLogBuilder
7+
private let yield: (ReadableLogRecord) async -> Void
68
private var source: DispatchSourceMemoryPressure?
79

8-
init(options: Options, logsApi: LogsApi) {
10+
init(options: Options, appLogBuilder: AppLogBuilder, yield: @escaping (ReadableLogRecord) async -> Void) {
911
self.options = options
10-
self.logsApi = logsApi
12+
self.appLogBuilder = appLogBuilder
13+
self.yield = yield
1114
}
1215

1316
func start() {
@@ -23,20 +26,22 @@ final class MemoryPressureMonitor: AutoInstrumentation {
2326
eventMask: [.critical, .warning],
2427
queue: .global(qos: .background)
2528
)
29+
let localYield = yield
2630
source?.setEventHandler { [weak self] in
2731
Task {
2832
guard let self, let event = self.source?.data else { return }
2933
/// Report only if memory pressure is warning or critical
3034
guard [DispatchSource.MemoryPressureEvent.warning, .critical].contains(event) else {
3135
return
3236
}
33-
self.logsApi.recordLog(
34-
message: "applicationDidReceiveMemoryWarning",
35-
severity: .warn,
36-
attributes: [
37-
SemanticConvention.systemMemoryWarning: .string(event.name)
38-
]
39-
)
37+
38+
guard let log = self.appLogBuilder.buildLog(message: "applicationDidReceiveMemoryWarning",
39+
severity: .warn,
40+
attributes: [SemanticConvention.systemMemoryWarning: .string(event.name)]) else {
41+
return
42+
}
43+
44+
await localYield(log)
4045
}
4146
}
4247
source?.activate()

Sources/Observability/Client/ObservabilityClient.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,4 @@ extension ObservabilityClient: Observe {
6262
func startSpan(name: String, attributes: [String : AttributeValue]) -> any Span {
6363
tracer.startSpan(name: name, attributes: attributes)
6464
}
65-
66-
func flush() -> Bool {
67-
tracer.flush() &&
68-
meter.flush() &&
69-
logger.flush()
70-
}
7165
}

Sources/Observability/Client/ObservabilityClientFactory.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,25 @@ public struct ObservabilityClientFactory {
4646
}
4747

4848
if options.logs == .enabled {
49-
logger = LoggerDecorator(options: options, sessionManager: sessionManager, eventQueue: eventQueue, sampler: sampler)
49+
let appLogBuilder = AppLogBuilder(options: options, sessionManager: sessionManager, sampler: sampler)
50+
logger = LoggerDecorator(eventQueue: eventQueue, appLogBuilder: appLogBuilder)
5051
let logExporter = OtlpLogExporter(endpoint: url)
5152
Task {
5253
await batchWorker.addExporter(logExporter)
5354
}
5455
if options.autoInstrumentation.contains(.memoryWarnings) {
55-
let memoryWarningMonitor = MemoryPressureMonitor(options: options, logsApi: logger)
56+
let memoryWarningMonitor = MemoryPressureMonitor(options: options, appLogBuilder: appLogBuilder) { log in
57+
await eventQueue.send(EventQueueItem(payload: LogItem(log: log)))
58+
}
5659
autoInstrumentation.append(memoryWarningMonitor)
5760
}
61+
62+
let appLifecycleLogger = AppLifecycleLogger(appLifecycleManager: appLifecycleManager, appLogBuilder: appLogBuilder) { log in
63+
Task {
64+
await eventQueue.send(EventQueueItem(payload: LogItem(log: log)))
65+
}
66+
}
67+
autoInstrumentation.append(appLifecycleLogger)
5868
} else {
5969
logger = NoOpLogger()
6070
}
@@ -124,6 +134,7 @@ public struct ObservabilityClientFactory {
124134
}
125135
)
126136
}
137+
127138
if options.autoInstrumentation.contains(.cpu) {
128139
autoInstrumentation.append(
129140
MeasurementTask(metricsApi: meter, samplingInterval: autoInstrumentationSamplingInterval) { api in

Sources/Observability/CrashReports/LDCrashFilter.swift

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,7 @@ final class LDCrashFilter: NSObject, CrashReportFilter {
6666
attributes: attributes
6767
)
6868
}
69-
70-
Task { [weak self] in
71-
guard self?.logsApi.flush() == true else {
72-
onCompletion?(reports, LaunchDarklyCrashFilterError.flushFailed)
73-
return
74-
}
75-
onCompletion?(reports, nil)
76-
}
69+
onCompletion?(reports, nil)
7770
} catch let error {
7871
onCompletion?(reports, error)
7972
}

Sources/Observability/Logs/LDLogRecordBuilder.swift

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22
import OpenTelemetryApi
33
import OpenTelemetrySdk
44

5-
class LDLogRecordBuilder: EventBuilder {
5+
final class LDLogRecordBuilder {
66
private var limits: LogLimits
77
private var instrumentationScope: InstrumentationScopeInfo
88
private var includeSpanContext: Bool
@@ -14,16 +14,13 @@ class LDLogRecordBuilder: EventBuilder {
1414
private var spanContext: SpanContext?
1515
private var resource: Resource
1616
private var clock: Clock
17-
private var queue: EventQueuing
1817
private let sampler: ExportSampler
1918

20-
init(queue: EventQueuing,
21-
sampler: ExportSampler,
22-
resource: Resource,
23-
clock: Clock,
24-
instrumentationScope: InstrumentationScopeInfo,
25-
includeSpanContext: Bool) {
26-
self.queue = queue
19+
init(sampler: ExportSampler,
20+
resource: Resource,
21+
clock: Clock,
22+
instrumentationScope: InstrumentationScopeInfo,
23+
includeSpanContext: Bool) {
2724
self.sampler = sampler
2825
self.resource = resource
2926
self.clock = clock
@@ -33,6 +30,32 @@ class LDLogRecordBuilder: EventBuilder {
3330
self.instrumentationScope = instrumentationScope
3431
self.attributes = AttributesDictionary(capacity: logLimits.maxAttributeCount,
3532
valueLengthLimit: logLimits.maxAttributeLength)
33+
34+
}
35+
36+
public func readableLogRecord() -> ReadableLogRecord? {
37+
if spanContext == nil, includeSpanContext {
38+
spanContext = OpenTelemetry.instance.contextProvider.activeSpan?.context
39+
}
40+
41+
let attrs = attributes.reduce(into: [String: OpenTelemetryApi.AttributeValue]()) { result, element in
42+
result[element.0] = element.1
43+
}
44+
45+
let log = ReadableLogRecord(resource: resource,
46+
instrumentationScopeInfo: instrumentationScope,
47+
timestamp: timestamp ?? clock.now,
48+
observedTimestamp: observedTimestamp,
49+
spanContext: spanContext,
50+
severity: severity,
51+
body: body,
52+
attributes: attrs)
53+
54+
guard let sampledLog = sampler.sampledLog(log) else {
55+
return nil
56+
}
57+
58+
return sampledLog
3659
}
3760

3861
public func setTimestamp(_ timestamp: Date) -> Self {
@@ -69,31 +92,4 @@ class LDLogRecordBuilder: EventBuilder {
6992
self.attributes["event.data"] = OpenTelemetryApi.AttributeValue(AttributeSet(labels: attributes))
7093
return self
7194
}
72-
73-
public func emit() {
74-
if spanContext == nil, includeSpanContext {
75-
spanContext = OpenTelemetry.instance.contextProvider.activeSpan?.context
76-
}
77-
78-
Task {
79-
let attrs = attributes.reduce(into: [String: OpenTelemetryApi.AttributeValue]()) { result, element in
80-
result[element.0] = element.1
81-
}
82-
83-
let log = ReadableLogRecord(resource: resource,
84-
instrumentationScopeInfo: instrumentationScope,
85-
timestamp: timestamp ?? clock.now,
86-
observedTimestamp: observedTimestamp,
87-
spanContext: spanContext,
88-
severity: severity,
89-
body: body,
90-
attributes: attrs)
91-
92-
guard let sampledLog = sampler.sampledLog(log) else {
93-
return
94-
}
95-
96-
await queue.send(EventQueueItem(payload: LogItem(log: sampledLog)))
97-
}
98-
}
9995
}
Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
import Foundation
22
import OpenTelemetrySdk
33

4-
final class LoggerDecorator: Logger {
4+
final class AppLogBuilder {
55
private let options: Options
6-
private var logger: any Logger { self }
76
private let sessionManager: SessionManager
8-
private let eventQueue: EventQueue
97
private let sampler: ExportSampler
108

119
init(
1210
options: Options,
1311
sessionManager: SessionManager,
14-
eventQueue: EventQueue,
1512
sampler: ExportSampler
1613
) {
1714
self.options = options
1815
self.sessionManager = sessionManager
19-
self.eventQueue = eventQueue
2016
self.sampler = sampler
2117
}
2218

@@ -25,13 +21,38 @@ final class LoggerDecorator: Logger {
2521
DefaultLoggerProvider.instance.get(instrumentationScopeName: "").eventBuilder(name: name)
2622
}
2723

28-
func logRecordBuilder() -> LogRecordBuilder {
29-
LDLogRecordBuilder(queue: eventQueue,
30-
sampler: sampler,
31-
resource: Resource(attributes: options.resourceAttributes),
32-
clock: MillisClock(),
33-
instrumentationScope: .init(name: options.serviceName),
34-
includeSpanContext: true)
24+
public func buildLog(message: String,
25+
severity: Severity,
26+
attributes: [String: AttributeValue]) -> ReadableLogRecord? {
27+
var attributes = attributes
28+
let sessionId = sessionManager.sessionInfo.id
29+
if !sessionId.isEmpty {
30+
attributes[SemanticConvention.highlightSessionId] = .string(sessionId)
31+
}
32+
33+
let logBuilder = LDLogRecordBuilder(
34+
sampler: sampler,
35+
resource: Resource(attributes: options.resourceAttributes),
36+
clock: MillisClock(),
37+
instrumentationScope: .init(name: options.serviceName),
38+
includeSpanContext: true)
39+
40+
logBuilder.setBody(.string(message))
41+
logBuilder.setTimestamp(Date())
42+
logBuilder.setSeverity(severity)
43+
logBuilder.setAttributes(attributes)
44+
45+
return logBuilder.readableLogRecord()
46+
}
47+
}
48+
49+
final class LoggerDecorator {
50+
private let eventQueue: EventQueue
51+
private let appLogBuilder: AppLogBuilder
52+
53+
init(eventQueue: EventQueue, appLogBuilder: AppLogBuilder) {
54+
self.eventQueue = eventQueue
55+
self.appLogBuilder = appLogBuilder
3556
}
3657
}
3758

@@ -41,22 +62,14 @@ extension LoggerDecorator: LogsApi {
4162
severity: Severity,
4263
attributes: [String: AttributeValue]
4364
) {
44-
var attributes = attributes
45-
let sessionId = sessionManager.sessionInfo.id
46-
if !sessionId.isEmpty {
47-
attributes[SemanticConvention.highlightSessionId] = .string(sessionId)
65+
Task {
66+
guard let log = appLogBuilder.buildLog(message: message,
67+
severity: severity,
68+
attributes: attributes) else {
69+
return
70+
}
71+
72+
await eventQueue.send(EventQueueItem(payload: LogItem(log: log)))
4873
}
49-
logRecordBuilder()
50-
.setBody(.string(message))
51-
.setTimestamp(Date())
52-
.setSeverity(severity)
53-
.setAttributes(attributes)
54-
.emit()
55-
}
56-
57-
public func flush() -> Bool {
58-
// TODO: Implement flush API since we are using LDLogRecordBuilder now.
59-
// logRecordProcessor.forceFlush(explicitTimeout: CommonOTelConfiguration.flushTimeout) == .success
60-
true
6174
}
6275
}

0 commit comments

Comments
 (0)