Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RUMM-2735 Add methods to pass error info to logs as decomposed strings #1051

Merged
merged 4 commits into from
Nov 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
60 changes: 60 additions & 0 deletions Sources/Datadog/Logger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@ 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 or 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 +209,23 @@ 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 @@ -421,6 +461,25 @@ 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 @@ -448,6 +507,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 @@ -95,6 +95,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 configuration.sampler.sample() else {
return
}
Expand Down Expand Up @@ -135,7 +150,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
28 changes: 28 additions & 0 deletions Tests/DatadogTests/Datadog/LoggerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,34 @@ 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: - Sampling

func testSamplingEnabled() {
Expand Down
40 changes: 40 additions & 0 deletions Tests/DatadogTests/Datadog/Logging/ConsoleLoggerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,44 @@ 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)
}
}