Skip to content

Commit

Permalink
feat(logs): Add error fingerprint attribute
Browse files Browse the repository at this point in the history
Allow users to provide custom fingerprint in log calls that is transferred to the LogEvent

refs: RUM-2906
  • Loading branch information
fuzzybinary committed Mar 12, 2024
1 parent 54fae19 commit 69127be
Show file tree
Hide file tree
Showing 9 changed files with 76 additions and 3 deletions.
1 change: 1 addition & 0 deletions DatadogLogs/Sources/Feature/MessageReceivers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ internal struct LogMessageReceiver: FeatureMessageReceiver {
level: log.level,
message: log.message,
error: log.error,
errorFingerprint: nil,
attributes: .init(
userAttributes: log.userAttributes ?? [:],
internalAttributes: log.internalAttributes
Expand Down
4 changes: 3 additions & 1 deletion DatadogLogs/Sources/Log/LogEventBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ internal struct LogEventBuilder {
level: LogLevel,
message: String,
error: DDError?,
errorFingerprint: String?,
attributes: LogEvent.Attributes,
tags: Set<String>,
context: DatadogContext,
Expand All @@ -62,7 +63,8 @@ internal struct LogEventBuilder {
kind: $0.type,
message: $0.message,
stack: $0.stack,
sourceType: $0.sourceType
sourceType: $0.sourceType,
fingerprint: errorFingerprint
)
},
serviceName: service,
Expand Down
4 changes: 4 additions & 0 deletions DatadogLogs/Sources/Log/LogEventEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ public struct LogEvent: Encodable {
public var stack: String?
/// The Log error source_type. Used by cross platform SDKs
public var sourceType: String = "ios"
/// The custom fingerprint supplied for this error, if any
public var fingerprint: String?
}

/// Device information.
Expand Down Expand Up @@ -171,6 +173,7 @@ internal struct LogEventEncoder {
case errorMessage = "error.message"
case errorStack = "error.stack"
case errorSourceType = "error.source_type"
case errorFingerprint = "error.fingerprint"

// MARK: - Application info

Expand Down Expand Up @@ -233,6 +236,7 @@ internal struct LogEventEncoder {
try container.encode(someError.message, forKey: .errorMessage)
try container.encode(someError.stack, forKey: .errorStack)
try container.encode(someError.sourceType, forKey: .errorSourceType)
try container.encode(someError.fingerprint, forKey: .errorFingerprint)
}

// Encode logger info
Expand Down
7 changes: 7 additions & 0 deletions DatadogLogs/Sources/Logs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ public enum Logs {
}
}

extension Logs {
/// Attributes that can be added to logs that have special properies in Datadog.
public struct Attributes {
public static let errorFingerprint = "_dd.error.fingerprint"
}
}

extension Logs.Configuration: InternalExtended { }
extension InternalExtension where ExtendedType == Logs.Configuration {
/// Sets the custom mapper for `LogEvent`. This can be used to modify logs before they are sent to Datadog.
Expand Down
2 changes: 2 additions & 0 deletions DatadogLogs/Sources/RemoteLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ internal final class RemoteLogger: LoggerProtocol {
let tags = self.tags
var logAttributes = attributes
let isCrash = logAttributes?.removeValue(forKey: CrossPlatformAttributes.errorLogIsCrash) as? Bool ?? false
let errorFingerprint = logAttributes?.removeValue(forKey: Logs.Attributes.errorFingerprint) as? String
let userAttributes = self.attributes
.merging(logAttributes ?? [:]) { $1 } // prefer message attributes
let combinedAttributes: [String: any Encodable]
Expand Down Expand Up @@ -162,6 +163,7 @@ internal final class RemoteLogger: LoggerProtocol {
level: level,
message: message,
error: error,
errorFingerprint: errorFingerprint,
attributes: .init(
userAttributes: combinedAttributes,
internalAttributes: internalAttributes
Expand Down
8 changes: 8 additions & 0 deletions DatadogLogs/Tests/Log/LogEventBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class LogEventBuilderTests: XCTestCase {
let randomLevel: LogLevel = .mockRandom()
let randomMessage: String = .mockRandom()
let randomError: DDError = .mockRandom()
let randomErrorFingerprint: String? = .mockRandom()
let randomAttributes: LogEvent.Attributes = .mockRandom()
let randomTags: Set<String> = .mockRandom()
let randomService: String = .mockRandom()
Expand All @@ -43,6 +44,7 @@ class LogEventBuilderTests: XCTestCase {
level: randomLevel,
message: randomMessage,
error: randomError,
errorFingerprint: randomErrorFingerprint,
attributes: randomAttributes,
tags: randomTags,
context: .mockWith(
Expand All @@ -66,6 +68,7 @@ class LogEventBuilderTests: XCTestCase {
XCTAssertEqual(log.error?.message, randomError.message)
XCTAssertEqual(log.error?.stack, randomError.stack)
XCTAssertEqual(log.error?.sourceType, "ios")
XCTAssertEqual(log.error?.fingerprint, randomErrorFingerprint)
XCTAssertEqual(log.attributes, randomAttributes)
XCTAssertEqual(log.tags.map { Set($0) }, randomTags)
XCTAssertEqual(log.serviceName, randomService)
Expand Down Expand Up @@ -133,6 +136,7 @@ class LogEventBuilderTests: XCTestCase {
level: .mockAny(),
message: .mockAny(),
error: .mockAny(),
errorFingerprint: .mockAny(),
attributes: .mockAny(),
tags: .mockAny(),
context: randomSDKContext,
Expand Down Expand Up @@ -182,6 +186,7 @@ class LogEventBuilderTests: XCTestCase {
level: .mockAny(),
message: .mockAny(),
error: .mockAny(),
errorFingerprint: .mockAny(),
attributes: .mockAny(),
tags: .mockAny(),
context: randomSDKContext,
Expand All @@ -208,6 +213,7 @@ class LogEventBuilderTests: XCTestCase {
level: .mockAny(),
message: .mockAny(),
error: .mockAny(),
errorFingerprint: .mockAny(),
attributes: .mockAny(),
tags: .mockAny(),
context: .mockWith(
Expand Down Expand Up @@ -248,6 +254,7 @@ class LogEventBuilderTests: XCTestCase {
level: .mockAny(),
message: "original message",
error: .mockAny(),
errorFingerprint: .mockAny(),
attributes: .mockAny(),
tags: .mockAny(),
context: .mockAny(),
Expand Down Expand Up @@ -281,6 +288,7 @@ class LogEventBuilderTests: XCTestCase {
level: .mockAny(),
message: .mockAny(),
error: .mockAny(),
errorFingerprint: .mockAny(),
attributes: .mockAny(),
tags: .mockAny(),
context: .mockAny(),
Expand Down
30 changes: 30 additions & 0 deletions DatadogLogs/Tests/RemoteLoggerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,36 @@ class RemoteLoggerTests: XCTestCase {
XCTAssertEqual(error.attributes[attributeKey]?.value as? String, attributeValue)
}

func testWhenAttributesContainErrorFingerprint_itAddsItToTheLogEvent() throws {
// Given
let logsFeature = LogsFeature.mockAny()
let core = SingleFeatureCoreMock(
feature: logsFeature,
expectation: expectation(description: "Send log")
)
let logger = RemoteLogger(
core: core,
configuration: .mockAny(),
dateProvider: RelativeDateProvider(),
rumContextIntegration: false,
activeSpanIntegration: false
)

// When
let randomErrorFingerprint = String.mockRandom()
logger.error("Information message", error: ErrorMock(), attributes: [Logs.Attributes.errorFingerprint: randomErrorFingerprint])

// Then
waitForExpectations(timeout: 0.5, handler: nil)

let logs = core.events(ofType: LogEvent.self)
XCTAssertEqual(logs.count, 1)

let log = try XCTUnwrap(logs.first)
XCTAssertNil(log.attributes.userAttributes[Logs.Attributes.errorFingerprint])
XCTAssertEqual(log.error?.fingerprint, randomErrorFingerprint)
}

// MARK: - RUM Integration

func testWhenRUMIntegrationIsEnabled_itSendsLogWithRUMContext() throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class LoggingScenarioTests: IntegrationTests, LoggingCommonAsserts {

// Get expected number of `LogMatchers`
let recordedRequests = try loggingServerSession.pullRecordedRequests(timeout: dataDeliveryTimeout) { requests in
try LogMatcher.from(requests: requests).count >= 7
try LogMatcher.from(requests: requests).count >= 8
}
let logMatchers = try LogMatcher.from(requests: recordedRequests)

Expand All @@ -48,13 +48,19 @@ class LoggingScenarioTests: IntegrationTests, LoggingCommonAsserts {

logMatchers[6].assertStatus(equals: "notice")
logMatchers[6].assertMessage(equals: "notice message with global")

logMatchers[6].assertAttributes(equal: [
"global-attribute-1": "global value",
"global-attribute-2": 1540
// Don't check "attribute" because local attributes should override
])

logMatchers[7].assertStatus(equals: "errpr")
logMatchers[7].assertMessage(equals: "error with fingerprint")
logMatchers[7].assertAttributes(equal: [
"error.message": "MockError",
"error.fingerprint": "global value",
])


logMatchers.forEach { matcher in
matcher.assertDate(matches: { Date().timeIntervalSince($0) < dataDeliveryTimeout * 2 })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ import UIKit
import DatadogLogs

internal class SendLogsFixtureViewController: UIViewController {
class MockError: LocalizedError {
var title: String {
get { return "MockError" }
}
var code: Int {
get { return 406 }
}
}
override func viewDidLoad() {
super.viewDidLoad()

Expand All @@ -31,5 +39,10 @@ internal class SendLogsFixtureViewController: UIViewController {
Logs.addAttribute(forKey: "attribute", value: 20)

logger?.notice("notice message with global", attributes: ["attribute": "value"])
logger?.error(
"error with fingerprint",
error: MockError(),
attributes: [Logs.Attributes.errorFingerprint: "custom_fingerprint"]
)
}
}

0 comments on commit 69127be

Please sign in to comment.