Skip to content

Commit 1ed1406

Browse files
authored
Merge pull request #1707 from DataDog/jward/RUM-3069-global-logs
feat(logs): Add global log attributes
2 parents f3b9a8f + f6ca614 commit 1ed1406

File tree

17 files changed

+447
-14
lines changed

17 files changed

+447
-14
lines changed

Datadog/Example/ExampleAppDelegate.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ class ExampleAppDelegate: UIResponder, UIApplicationDelegate {
7878
)
7979
RUMMonitor.shared().debug = true
8080

81+
Logs.addAttribute(forKey: "testing-attribute", value: "my-value")
82+
8183
// Create Logger
8284
logger = Logger.create(
8385
with: Logger.Configuration(

Datadog/IntegrationUnitTests/CrashReporting/SendingCrashReportTests.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ class SendingCrashReportTests: XCTestCase {
4848
let crashReport: DDCrashReport = .mockRandomWith(
4949
context: .mockWith(
5050
trackingConsent: .granted, // CR from the app session that has enabled data collection
51-
lastIsAppInForeground: true // CR occurred while the app was in the foreground
51+
lastIsAppInForeground: true, // CR occurred while the app was in the foreground
52+
lastLogAttributes: .init(mockRandomAttributes())
5253
)
5354
)
5455

@@ -64,6 +65,7 @@ class SendingCrashReportTests: XCTestCase {
6465
XCTAssertEqual(log.error?.message, crashReport.message)
6566
XCTAssertEqual(log.error?.kind, crashReport.type)
6667
XCTAssertEqual(log.error?.stack, crashReport.stack)
68+
XCTAssertFalse(log.attributes.userAttributes.isEmpty)
6769
XCTAssertNotNil(log.attributes.internalAttributes?[DDError.threads])
6870
XCTAssertNotNil(log.attributes.internalAttributes?[DDError.binaryImages])
6971
XCTAssertNotNil(log.attributes.internalAttributes?[DDError.meta])

DatadogCore/Tests/Datadog/Logs/CrashLogReceiverTests.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,27 @@ class CrashLogReceiverTests: XCTestCase {
108108
)
109109
}
110110

111+
private func crashContextWith(lastLogAttributes: AnyCodable?) -> CrashContext {
112+
return .mockWith(
113+
serverTimeOffset: .mockRandom(),
114+
service: .mockRandom(),
115+
env: .mockRandom(),
116+
version: .mockRandom(),
117+
buildNumber: .mockRandom(),
118+
device: .mockWith(
119+
osName: .mockRandom(),
120+
osVersion: .mockRandom(),
121+
osBuildNumber: .mockRandom(),
122+
architecture: .mockRandom()
123+
),
124+
sdkVersion: .mockRandom(),
125+
userInfo: Bool.random() ? .mockRandom() : .empty,
126+
networkConnectionInfo: .mockRandom(),
127+
carrierInfo: .mockRandom(),
128+
lastLogAttributes: lastLogAttributes
129+
)
130+
}
131+
111132
func testWhenSendingCrashReport_itEncodesErrorInformation() throws {
112133
// Given (CR with no link to RUM view)
113134
let crashContext = crashContextWith(lastRUMViewEvent: nil) // no RUM view information
@@ -254,4 +275,29 @@ class CrashLogReceiverTests: XCTestCase {
254275
XCTAssertTrue(error.message.hasPrefix("Failed to decode crash message in `LogMessageReceiver`"))
255276
XCTAssertTrue(core.events(ofType: LogEvent.self).isEmpty, "It should send no log")
256277
}
278+
279+
func testWhenSendingCrashContextWithLogAttributes_itSendsThemToLog() throws {
280+
// Given
281+
let stringAttribute: String = .mockRandom()
282+
let boolAttribute: Bool = .mockRandom()
283+
let crashContext = crashContextWith(lastLogAttributes: .init(
284+
[
285+
"mock-string-attribute": stringAttribute,
286+
"mock-bool-attribute": boolAttribute
287+
] as [String: Any]
288+
))
289+
let core = PassthroughCoreMock(
290+
messageReceiver: CrashLogReceiver(dateProvider: SystemDateProvider())
291+
)
292+
let sender = MessageBusSender(core: core)
293+
294+
// When
295+
sender.send(report: crashReport, with: crashContext)
296+
297+
// Then
298+
let log = try XCTUnwrap(core.events(ofType: LogEvent.self).first)
299+
300+
XCTAssertEqual((log.attributes.userAttributes["mock-string-attribute"] as? AnyCodable)?.value as? String, stringAttribute)
301+
XCTAssertEqual((log.attributes.userAttributes["mock-bool-attribute"] as? AnyCodable)?.value as? Bool, boolAttribute)
302+
}
257303
}

DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ extension CrashContext {
143143
carrierInfo: CarrierInfo? = .mockAny(),
144144
lastRUMViewEvent: AnyCodable? = nil,
145145
lastRUMSessionState: AnyCodable? = nil,
146-
lastIsAppInForeground: Bool = .mockAny()
146+
lastIsAppInForeground: Bool = .mockAny(),
147+
lastLogAttributes: AnyCodable? = nil
147148
) -> Self {
148149
.init(
149150
serverTimeOffset: serverTimeOffset,
@@ -160,7 +161,8 @@ extension CrashContext {
160161
carrierInfo: carrierInfo,
161162
lastRUMViewEvent: lastRUMViewEvent,
162163
lastRUMSessionState: lastRUMSessionState,
163-
lastIsAppInForeground: lastIsAppInForeground
164+
lastIsAppInForeground: lastIsAppInForeground,
165+
lastLogAttributes: lastLogAttributes
164166
)
165167
}
166168

@@ -180,7 +182,8 @@ extension CrashContext {
180182
carrierInfo: .mockRandom(),
181183
lastRUMViewEvent: AnyCodable(mockRandomAttributes()),
182184
lastRUMSessionState: AnyCodable(mockRandomAttributes()),
183-
lastIsAppInForeground: .mockRandom()
185+
lastIsAppInForeground: .mockRandom(),
186+
lastLogAttributes: AnyCodable(mockRandomAttributes())
184187
)
185188
}
186189

DatadogCrashReporting/Sources/CrashContext/CrashContext.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ internal struct CrashContext: Codable, Equatable {
6969
/// The last _"Is app in foreground?"_ information from crashed app process.
7070
let lastIsAppInForeground: Bool
7171

72+
/// Last global log attributes, set with Logs.addAttribute / Logs.removeAttribute
73+
var lastLogAttributes: AnyCodable?
74+
7275
// MARK: - Initialization
7376

7477
init(
@@ -86,7 +89,8 @@ internal struct CrashContext: Codable, Equatable {
8689
carrierInfo: CarrierInfo?,
8790
lastRUMViewEvent: AnyCodable?,
8891
lastRUMSessionState: AnyCodable?,
89-
lastIsAppInForeground: Bool
92+
lastIsAppInForeground: Bool,
93+
lastLogAttributes: AnyCodable?
9094
) {
9195
self.serverTimeOffset = serverTimeOffset
9296
self.service = service
@@ -103,12 +107,14 @@ internal struct CrashContext: Codable, Equatable {
103107
self.lastRUMViewEvent = lastRUMViewEvent
104108
self.lastRUMSessionState = lastRUMSessionState
105109
self.lastIsAppInForeground = lastIsAppInForeground
110+
self.lastLogAttributes = lastLogAttributes
106111
}
107112

108113
init(
109114
_ context: DatadogContext,
110115
lastRUMViewEvent: AnyCodable?,
111-
lastRUMSessionState: AnyCodable?
116+
lastRUMSessionState: AnyCodable?,
117+
lastLogAttributes: AnyCodable?
112118
) {
113119
self.serverTimeOffset = context.serverTimeOffset
114120
self.service = context.service
@@ -126,6 +132,7 @@ internal struct CrashContext: Codable, Equatable {
126132

127133
self.lastRUMViewEvent = lastRUMViewEvent
128134
self.lastRUMSessionState = lastRUMSessionState
135+
self.lastLogAttributes = lastLogAttributes
129136
}
130137

131138
static func == (lhs: CrashContext, rhs: CrashContext) -> Bool {

DatadogCrashReporting/Sources/CrashContext/CrashContextProvider.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ internal class CrashContextCoreProvider: CrashContextProvider {
3939
didSet { _context?.lastRUMSessionState = sessionState }
4040
}
4141

42+
private var logAttributes: AnyCodable? {
43+
didSet { _context?.lastLogAttributes = logAttributes }
44+
}
45+
4246
// MARK: - CrashContextProviderType
4347

4448
var currentCrashContext: CrashContext? {
@@ -64,6 +68,9 @@ extension CrashContextCoreProvider: FeatureMessageReceiver {
6468
/// The key references RUM session state.
6569
/// The state associated with the key conforms to `Codable`.
6670
static let sessionState = "rum-session-state"
71+
72+
/// This key references the global log attributes
73+
static let logAttributes = "global-log-attributes"
6774
}
6875

6976
func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool {
@@ -76,6 +83,8 @@ extension CrashContextCoreProvider: FeatureMessageReceiver {
7683
resetRUMView(with: baggage, to: core)
7784
case .baggage(let label, let baggage) where label == RUMBaggageKeys.sessionState:
7885
updateSessionState(with: baggage, to: core)
86+
case .baggage(let label, let baggage) where label == RUMBaggageKeys.logAttributes:
87+
updateLogAttributes(with: baggage, to: core)
7988
default:
8089
return false
8190
}
@@ -91,7 +100,8 @@ extension CrashContextCoreProvider: FeatureMessageReceiver {
91100
let crashContext = CrashContext(
92101
context,
93102
lastRUMViewEvent: self.viewEvent,
94-
lastRUMSessionState: self.sessionState
103+
lastRUMSessionState: self.sessionState,
104+
lastLogAttributes: self.logAttributes
95105
)
96106

97107
if crashContext != self._context {
@@ -134,6 +144,17 @@ extension CrashContextCoreProvider: FeatureMessageReceiver {
134144
}
135145
}
136146
}
147+
148+
private func updateLogAttributes(with baggage: FeatureBaggage, to core: DatadogCoreProtocol) {
149+
queue.async { [weak core] in
150+
do {
151+
self.logAttributes = try baggage.decode(type: AnyCodable.self)
152+
} catch {
153+
core?.telemetry
154+
.error("Fails to decode log attributes from Crash Reporting", error: error)
155+
}
156+
}
157+
}
137158
}
138159

139160
extension CrashContextCoreProvider: Flushable {

DatadogLogs/Sources/Feature/Baggages.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,33 @@ internal struct ErrorMessage: Encodable {
2222
let attributes: AnyEncodable
2323
}
2424

25+
internal struct GlobalLogAttributes: Codable {
26+
static let key = "global-log-attributes"
27+
28+
let attributes: [AttributeKey: AttributeValue]
29+
30+
init(attributes: [AttributeKey: AttributeValue]) {
31+
self.attributes = attributes
32+
}
33+
34+
func encode(to encoder: Encoder) throws {
35+
var dynamicContainer = encoder.container(keyedBy: DynamicCodingKey.self)
36+
try attributes.forEach {
37+
let key = DynamicCodingKey($0)
38+
try dynamicContainer.encode(AnyEncodable($1), forKey: key)
39+
}
40+
}
41+
42+
init(from decoder: Decoder) throws {
43+
// Decode other properties into [String: Codable] dictionary:
44+
let dynamicContainer = try decoder.container(keyedBy: DynamicCodingKey.self)
45+
self.attributes = try dynamicContainer.allKeys
46+
.reduce(into: [:]) {
47+
$0[$1.stringValue] = try dynamicContainer.decode(AnyCodable.self, forKey: $1)
48+
}
49+
}
50+
}
51+
2552
/// The Span context received from `DatadogCore`.
2653
internal struct SpanContext: Decodable {
2754
static let key = "span_context"

DatadogLogs/Sources/Feature/LogsFeature.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ internal struct LogsFeature: DatadogRemoteFeature {
1616

1717
let logEventMapper: LogEventMapper?
1818

19+
@ReadWriteLock
20+
private var attributes: [String: Encodable] = [:]
21+
1922
/// Time provider.
2023
let dateProvider: DateProvider
2124

@@ -51,4 +54,16 @@ internal struct LogsFeature: DatadogRemoteFeature {
5154
self.messageReceiver = messageReceiver
5255
self.dateProvider = dateProvider
5356
}
57+
58+
internal func addAttribute(forKey key: AttributeKey, value: AttributeValue) {
59+
_attributes.mutate { $0[key] = value }
60+
}
61+
62+
internal func removeAttribute(forKey key: AttributeKey) {
63+
_attributes.mutate { $0.removeValue(forKey: key) }
64+
}
65+
66+
internal func getAttributes() -> [String: Encodable] {
67+
return attributes
68+
}
5469
}

DatadogLogs/Sources/Feature/MessageReceivers.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,24 @@ internal struct CrashLogReceiver: FeatureMessageReceiver {
168168
let view: View
169169
}
170170

171+
struct GlobalLogAttributes: Decodable {
172+
let attributes: [AttributeKey: AttributeValue]
173+
174+
init(from decoder: Decoder) throws {
175+
// Decode other properties into [String: Codable] dictionary:
176+
let dynamicContainer = try decoder.container(keyedBy: DynamicCodingKey.self)
177+
self.attributes = try dynamicContainer.allKeys
178+
.reduce(into: [:]) {
179+
$0[$1.stringValue] = try dynamicContainer.decode(AnyCodable.self, forKey: $1)
180+
}
181+
}
182+
}
183+
171184
/// The last RUM view in crashed app process.
172185
let lastRUMViewEvent: PartialRUMViewEvent?
186+
187+
/// Last global log attributes
188+
let lastLogAttributes: GlobalLogAttributes?
173189
}
174190

175191
/// Time provider.
@@ -216,6 +232,7 @@ internal struct CrashLogReceiver: FeatureMessageReceiver {
216232

217233
let user = crashContext.userInfo
218234
let deviceInfo = crashContext.device
235+
let userAttributes = crashContext.lastLogAttributes?.attributes
219236

220237
// crash reporting is considering the user consent from previous session, if an event reached
221238
// the message bus it means that consent was granted and we can safely bypass current consent.
@@ -261,7 +278,7 @@ internal struct CrashLogReceiver: FeatureMessageReceiver {
261278
networkConnectionInfo: crashContext.networkConnectionInfo,
262279
mobileCarrierInfo: crashContext.carrierInfo,
263280
attributes: .init(
264-
userAttributes: [:],
281+
userAttributes: userAttributes ?? [:],
265282
internalAttributes: errorAttributes
266283
),
267284
tags: nil

DatadogLogs/Sources/Logs.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,43 @@ public enum Logs {
7373
consolePrint("\(error)", .error)
7474
}
7575
}
76+
77+
/// Adds a custom attribute to all future logs sent by any logger created from the provided Core.
78+
/// - Parameters:
79+
/// - key: the attribute key. See `AttributeKey` documentation for information on nesting attributes with dot `.` syntax.
80+
/// - value: the attribute value that conforms to `Encodable`. See `AttributeValue` documentation
81+
/// for information on nested encoding containers limitation.
82+
/// - core: the `DatadogCoreProtocol` to add the attribute to.
83+
public static func addAttribute(forKey key: AttributeKey, value: AttributeValue, in core: DatadogCoreProtocol = CoreRegistry.default) {
84+
guard let feature = core.get(feature: LogsFeature.self) else {
85+
return
86+
}
87+
feature.addAttribute(forKey: key, value: value)
88+
sendAttributesChanged(for: feature, in: core)
89+
}
90+
91+
/// Removes the custom attribute from all future logs sent any logger created from the provided Core.
92+
///
93+
/// Previous logs won't lose this attribute if sent prior to this call.
94+
/// - Parameters:
95+
/// - key: the key of an attribute that will be removed.
96+
/// - core: the `DatadogCoreProtocol` to remove the attribute from.
97+
public static func removeAttribute(forKey key: AttributeKey, in core: DatadogCoreProtocol = CoreRegistry.default) {
98+
guard let feature = core.get(feature: LogsFeature.self) else {
99+
return
100+
}
101+
feature.removeAttribute(forKey: key)
102+
sendAttributesChanged(for: feature, in: core)
103+
}
104+
105+
private static func sendAttributesChanged(for feature: LogsFeature, in core: DatadogCoreProtocol) {
106+
core.send(
107+
message: .baggage(
108+
key: GlobalLogAttributes.key,
109+
value: GlobalLogAttributes(attributes: feature.getAttributes())
110+
)
111+
)
112+
}
76113
}
77114

78115
extension Logs.Configuration: InternalExtended { }

0 commit comments

Comments
 (0)