Skip to content

Commit

Permalink
Add methods to pass error info to logs as decomposed strings
Browse files Browse the repository at this point in the history
This is mostly for Cross Platform SDKs that need to send in serialized error information.
  • Loading branch information
fuzzybinary committed Nov 14, 2022
1 parent 86be6ac commit 3873751
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 8 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Unreleased

- [IMPROVEMENT] Add a method for sending error attributes on logs as strings.

# 1.13.0 / 08-11-2022

- [IMPROVEMENT] Improve console logs when using `DDNoopRUMMonitor`. See [#1007][] (Thanks [@dfed][])
Expand Down
31 changes: 31 additions & 0 deletions Sources/Datadog/Logger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ public protocol LoggerProtocol {
/// the same key already exist in this logger, it will be overridden (only for this message).
func log(level: LogLevel, message: String, error: Error?, attributes: [String: Encodable]?)

/// General purpose logging method.
/// Sends a log with certain `level`, `message`, `errorKind`, `errorMessage`, `stackTrace` and `attributes`.
///
/// This method is meant for non-native or cross platform frameworks (such as React Native of Flutter) to send error information
/// to Datadog. Although it can be used directly, it is recommended to use other methods declared on `Logger`.
///
/// - Parameters:
/// - level: the log level
/// - message: the message to be logged
/// - errorKind: the kind of error reported
/// - errorMessage: the message attached to the error
/// - stackTrace: a string representation of the error's stack trace
/// - attributes: a dictionary of attributes (optional) to add for this message. If an attribute with
/// the same key already exist in this logger, it will be overridden (only for this message).
func log(level: LogLevel, message: String, errorKind: String?, errorMessage: String?,
stackTrace: String?, attributes: [String: Encodable]?)


// MARK: - Attributes

/// Adds a custom attribute to all future logs sent by this logger.
Expand Down Expand Up @@ -186,6 +204,12 @@ public class Logger: LoggerProtocol {
v2Logger.log(level: level, message: message, error: error, attributes: attributes)
}

public func log(level: LogLevel, message: String, errorKind: String?, errorMessage: String?,
stackTrace: String?, attributes: [String: Encodable]?) {
v2Logger.log(level: level, message: message, errorKind: errorKind, errorMessage: errorMessage,
stackTrace: stackTrace, attributes: attributes)
}

public func addAttribute(forKey key: AttributeKey, value: AttributeValue) {
v2Logger.addAttribute(forKey: key, value: value)
}
Expand Down Expand Up @@ -423,6 +447,12 @@ internal struct CombinedLogger: LoggerProtocol {
combinedLoggers.forEach { $0.log(level: level, message: message, error: error, attributes: attributes) }
}

func log(level: LogLevel, message: String, errorKind: String?, errorMessage: String?,
stackTrace: String?, attributes: [String : Encodable]?) {
combinedLoggers.forEach { $0.log(level: level, message: message, errorKind: errorKind, errorMessage: errorMessage,
stackTrace: stackTrace, attributes: attributes) }
}

func addAttribute(forKey key: AttributeKey, value: AttributeValue) {
combinedLoggers.forEach { $0.addAttribute(forKey: key, value: value) }
}
Expand Down Expand Up @@ -450,6 +480,7 @@ internal struct CombinedLogger: LoggerProtocol {

internal struct NOPLogger: LoggerProtocol {
func log(level: LogLevel, message: String, error: Error?, attributes: [String: Encodable]?) {}
func log(level: LogLevel, message: String, errorKind: String?, errorMessage: String?, stackTrace: String?, attributes: [String : Encodable]?) {}
func addAttribute(forKey key: AttributeKey, value: AttributeValue) {}
func removeAttribute(forKey key: AttributeKey) {}
func addTag(withKey key: String, value: String) {}
Expand Down
35 changes: 28 additions & 7 deletions Sources/Datadog/Logging/ConsoleLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,45 @@ internal final class ConsoleLogger: LoggerProtocol {
// MARK: - Logging

func log(level: LogLevel, message: String, error: Error?, attributes: [String: Encodable]?) {
var errorString: String? = nil
if let error = error {
let ddError = DDError(error: error)
errorString = buildErrorString(error: ddError)
}

internalLog(level: level, message: message, errorString: errorString)
}

func log(level: LogLevel, message: String, errorKind: String?, errorMessage: String?, stackTrace: String?, attributes: [String : Encodable]?) {
var errorString: String? = nil
if errorKind != nil || errorMessage != nil || stackTrace != nil {
// Cross platform frameworks don't necessarilly send all values for errors. Send empty strings
// for any values that are empty.
let ddError = DDError(type: errorKind ?? "", message: errorMessage ?? "", stack: stackTrace ?? "")
errorString = buildErrorString(error: ddError)
}

internalLog(level: level, message: message, errorString: errorString)
}

private func internalLog(level: LogLevel, message: String, errorString: String?) {
let time = timeFormatter.string(from: dateProvider.now)
let status = level.asLogStatus.rawValue.uppercased()

var log = "\(self.prefix)\(time) [\(status)] \(message)"

if let error = error {
log += "\n\nError details:\n\(buildErrorString(error: error))"
if let errorString = errorString {
log += "\n\nError details:\n\(errorString)"
}

printFunction(log)
}

private func buildErrorString(error: Error) -> String {
let dderror = DDError(error: error)
private func buildErrorString(error: DDError) -> String {
return """
→ type: \(dderror.type)
→ message: \(dderror.message)
→ stack: \(dderror.stack)
→ type: \(error.type)
→ message: \(error.message)
→ stack: \(error.stack)
"""
}

Expand Down
17 changes: 16 additions & 1 deletion Sources/Datadog/Logging/RemoteLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,21 @@ internal final class RemoteLogger: LoggerProtocol {
// MARK: - Logging

func log(level: LogLevel, message: String, error: Error?, attributes: [String: Encodable]?) {
internalLog(level: level, message: message, error: error.map { DDError(error: $0) }, attributes: attributes)
}

func log(level: LogLevel, message: String, errorKind: String?, errorMessage: String?, stackTrace: String?, attributes: [String : Encodable]?) {
var ddError: DDError?
if errorKind != nil || errorMessage != nil || stackTrace != nil {
// Cross platform frameworks don't necessarilly send all values for errors. Send empty strings
// for any values that are empty.
ddError = DDError(type: errorKind ?? "", message: errorMessage ?? "", stack: stackTrace ?? "")
}

internalLog(level: level, message: message, error: ddError, attributes: attributes)
}

func internalLog(level: LogLevel, message: String, error: DDError?, attributes: [String : Encodable]?) {
guard level.rawValue >= configuration.threshold.rawValue else {
return
}
Expand Down Expand Up @@ -136,7 +151,7 @@ internal final class RemoteLogger: LoggerProtocol {
date: date,
level: level,
message: message,
error: error.map { DDError(error: $0) },
error: error,
attributes: .init(
userAttributes: userAttributes,
internalAttributes: internalAttributes
Expand Down
25 changes: 25 additions & 0 deletions Tests/DatadogTests/Datadog/LoggerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,31 @@ class LoggerTests: XCTestCase {
}
}

func testLoggingErrorStrings() throws {
core.context = .mockAny()

let feature: LoggingFeature = .mockByRecordingLogMatchers()
core.register(feature: feature)


let logger = Logger.builder.build(in: core)
let errorKind = String.mockRandom()
let errorMessage = String.mockRandom()
let stackTrace = String.mockRandom()
logger.log(level: .info, message: .mockAny(), errorKind: errorKind, errorMessage: errorMessage,
stackTrace: stackTrace, attributes: nil)


let logMatchers = try feature.waitAndReturnLogMatchers(count: 1)
let logMatcher = logMatchers.first
XCTAssertNotNil(logMatcher)
if let logMatcher = logMatcher {
logMatcher.assertValue(forKeyPath: "error.kind", equals: errorKind)
logMatcher.assertValue(forKeyPath: "error.message", equals: errorMessage)
logMatcher.assertValue(forKeyPath: "error.stack", equals: stackTrace)
}
}

// MARK: - Sending user info

func testSendingUserInfo() throws {
Expand Down
33 changes: 33 additions & 0 deletions Tests/DatadogTests/Datadog/Logging/ConsoleLoggerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,37 @@ class ConsoleLoggerTests: XCTestCase {
}
XCTAssertEqual(mock.printedMessages.count, 6)
}

func testItPrintsErrorStringsWithExpectedFormat() {
// Given
let logger = ConsoleLogger(
configuration: .init(
timeZone: .UTC,
format: .short
),
dateProvider: RelativeDateProvider(
using: .mockDecember15th2019At10AMUTC()
),
printFunction: mock.print
)

let message = String.mockRandom()
let errorKind = String.mockRandom()
let errorMessage = String.mockRandom()
let stackTrace = String.mockRandom()

logger.log(level: .info, message: message, errorKind: errorKind, errorMessage: errorMessage,
stackTrace: stackTrace, attributes: nil)
// Then
let expectedMessage = """
10:00:00.000 [INFO] \(message)
Error details:
→ type: \(errorKind)
→ message: \(errorMessage)
→ stack: \(stackTrace)
"""
XCTAssertEqual(mock.printedMessages.first, expectedMessage)
XCTAssertEqual(mock.printedMessages.count, 1)
}
}

0 comments on commit 3873751

Please sign in to comment.