From 38737510be444d5fa16a30342997b803a43349a5 Mon Sep 17 00:00:00 2001 From: Jeff Ward Date: Mon, 14 Nov 2022 14:08:58 -0500 Subject: [PATCH] Add methods to pass error info to logs as decomposed strings This is mostly for Cross Platform SDKs that need to send in serialized error information. --- CHANGELOG.md | 2 ++ Sources/Datadog/Logger.swift | 31 ++++++++++++++++ Sources/Datadog/Logging/ConsoleLogger.swift | 35 +++++++++++++++---- Sources/Datadog/Logging/RemoteLogger.swift | 17 ++++++++- Tests/DatadogTests/Datadog/LoggerTests.swift | 25 +++++++++++++ .../Datadog/Logging/ConsoleLoggerTests.swift | 33 +++++++++++++++++ 6 files changed, 135 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f14965f295..81807be7b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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][]) diff --git a/Sources/Datadog/Logger.swift b/Sources/Datadog/Logger.swift index 9662fa135f..4dd74274c8 100644 --- a/Sources/Datadog/Logger.swift +++ b/Sources/Datadog/Logger.swift @@ -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. @@ -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) } @@ -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) } } @@ -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) {} diff --git a/Sources/Datadog/Logging/ConsoleLogger.swift b/Sources/Datadog/Logging/ConsoleLogger.swift index 8eb2da46a9..6ea183d2f4 100644 --- a/Sources/Datadog/Logging/ConsoleLogger.swift +++ b/Sources/Datadog/Logging/ConsoleLogger.swift @@ -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) """ } diff --git a/Sources/Datadog/Logging/RemoteLogger.swift b/Sources/Datadog/Logging/RemoteLogger.swift index 5826160008..e40e13e3c3 100644 --- a/Sources/Datadog/Logging/RemoteLogger.swift +++ b/Sources/Datadog/Logging/RemoteLogger.swift @@ -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 } @@ -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 diff --git a/Tests/DatadogTests/Datadog/LoggerTests.swift b/Tests/DatadogTests/Datadog/LoggerTests.swift index ae97ee89f9..a50bf29e1b 100644 --- a/Tests/DatadogTests/Datadog/LoggerTests.swift +++ b/Tests/DatadogTests/Datadog/LoggerTests.swift @@ -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 { diff --git a/Tests/DatadogTests/Datadog/Logging/ConsoleLoggerTests.swift b/Tests/DatadogTests/Datadog/Logging/ConsoleLoggerTests.swift index fb65b576fd..164ff4bddd 100644 --- a/Tests/DatadogTests/Datadog/Logging/ConsoleLoggerTests.swift +++ b/Tests/DatadogTests/Datadog/Logging/ConsoleLoggerTests.swift @@ -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) + } }