// Software License Agreement (BSD License) // // Copyright (c) 2010-2025, Deusty, LLC // All rights reserved. // // Redistribution and use of this software in source and binary forms, // with or without modification, are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // // * Neither the name of Deusty nor the names of its contributors may be used // to endorse or promote products derived from this software without specific // prior written permission of Deusty, LLC. import CocoaLumberjack import Logging extension Logging.Logger.Level { @inlinable var ddLogLevelAndFlag: (DDLogLevel, DDLogFlag) { switch self { case .trace: return (.verbose, .verbose) case .debug: return (.debug, .debug) case .info, .notice: return (.info, .info) case .warning: return (.warning, .warning) case .error, .critical: return (.error, .error) } } } extension DDLogMessage { /// Contains the swift-log details of a given log message. public struct SwiftLogInformation: Equatable, Sendable { /// Contains information about the swift-log logger that logged this message. public struct LoggerInformation: Equatable, Sendable { /// Contains the metadata from the various sources of on a logger. /// Currently this can be the logger itself, as well as its metadata provider public struct MetadataSources: Equatable, Sendable { /// The metadata of the swift-log logger that logged this message. public let logger: Logging.Logger.Metadata /// The metadata of the metadata provider on the swift-log logger that logged this message. public let provider: Logging.Logger.Metadata? } /// The label of the swift-log logger that logged this message. public let label: String /// The metadata of the swift-log logger that logged this message. public let metadataSources: MetadataSources /// The metadata of the swift-log logger that logged this message. @available(*, deprecated, renamed: "metadataSources.logger") public var metadata: Logging.Logger.Metadata { metadataSources.logger } } /// Contains information about the swift-log message thas was logged. public struct MessageInformation: Equatable, Sendable { /// The original swift-log message. public let message: Logging.Logger.Message /// The original swift-log level of the message. This could be more fine-grained than ``DDLogMessage/level`` & ``DDLogMessage/flag``. public let level: Logging.Logger.Level /// The original swift-log metadata of the message. public let metadata: Logging.Logger.Metadata? /// The original swift-log source of the message. public let source: String } /// The information about the swift-log logger that logged this message. public let logger: LoggerInformation /// The information about the swift-log message that was logged. public let message: MessageInformation /// Merges the metadata from all layers together. /// The metadata on the logger provides the base. /// Metadata from the logger's metadata provider (if any) trumps the base. /// Metadata from the logged message again trumps both the base and the metadata from the logger's metadata provider. /// Essentially: `logger.metadata < logger.metadataProvider < message.metadata` /// - Note: Accessing this property performs the merge! Accessing it multiple times can be a performance issue! public var mergedMetadata: Logging.Logger.Metadata { var merged = logger.metadataSources.logger if let providerMetadata = logger.metadataSources.provider { merged.merge(providerMetadata, uniquingKeysWith: { $1 }) } if let messageMetadata = message.metadata { merged.merge(messageMetadata, uniquingKeysWith: { $1 }) } return merged } } /// The swift-log information of this log message. This only exists for messages logged via swift-log. /// - SeeAlso: ``DDLogMessage/SwiftLogInformation`` @inlinable public var swiftLogInfo: SwiftLogInformation? { (self as? SwiftLogMessage)?._swiftLogInfo } } /// This class (intentionally internal) is only an "encapsulation" layer above ``DDLogMessage``. /// It's basically an implementation detail of ``DDLogMessage/swiftLogInfo``. @usableFromInline final class SwiftLogMessage: DDLogMessage, @unchecked Sendable { @usableFromInline let _swiftLogInfo: SwiftLogInformation @usableFromInline init(loggerLabel: String, loggerMetadata: Logging.Logger.Metadata, loggerProvidedMetadata: Logging.Logger.Metadata?, message: Logging.Logger.Message, level: Logging.Logger.Level, metadata: Logging.Logger.Metadata?, source: String, file: String, function: String, line: UInt) { _swiftLogInfo = .init(logger: .init(label: loggerLabel, metadataSources: .init(logger: loggerMetadata, provider: loggerProvidedMetadata)), message: .init(message: message, level: level, metadata: metadata, source: source)) let (ddLogLevel, ddLogFlag) = level.ddLogLevelAndFlag let msg = String(describing: message) super.init(format: msg, formatted: msg, // We have no chance in retrieving the original format here. level: ddLogLevel, flag: ddLogFlag, context: 0, file: file, function: function, line: line, tag: nil, options: .dontCopyMessage, // Swift will bridge to NSString. No need to make an additional copy. timestamp: nil) // Passing nil will make DDLogMessage create the timestamp which saves us the bridging between Date and NSDate. } // Not removed due to `@usableFromInline`. @usableFromInline @available(*, deprecated, renamed: "init(loggerLabel:loggerMetadata:loggerMetadata:message:level:metadata:source:file:function:line:)") convenience init(loggerLabel: String, loggerMetadata: Logging.Logger.Metadata, message: Logging.Logger.Message, level: Logging.Logger.Level, metadata: Logging.Logger.Metadata?, source: String, file: String, function: String, line: UInt) { self.init(loggerLabel: loggerLabel, loggerMetadata: loggerMetadata, loggerProvidedMetadata: nil, message: message, level: level, metadata: metadata, source: source, file: file, function: function, line: line) } override func isEqual(_ object: Any?) -> Bool { super.isEqual(object) && (object as? SwiftLogMessage)?._swiftLogInfo == _swiftLogInfo } } /// A swift-log ``LogHandler`` implementation that forwards messages to a given ``DDLog`` instance. public struct DDLogHandler: LogHandler { @usableFromInline struct Configuration: Sendable { @usableFromInline struct SyncLogging: Sendable { @usableFromInline let tresholdLevel: Logging.Logger.Level @usableFromInline let metadataKey: Logging.Logger.Metadata.Key } @usableFromInline let log: DDLog @usableFromInline let syncLogging: SyncLogging } @usableFromInline struct LoggerInfo: Sendable { @usableFromInline struct MetadataSources: Sendable { @usableFromInline var provider: Logging.Logger.MetadataProvider? @usableFromInline var logger: Logging.Logger.Metadata = [:] } @usableFromInline let label: String @usableFromInline var logLevel: Logging.Logger.Level @usableFromInline var metadataSources: MetadataSources // Not removed due to `@usableFromInline` @usableFromInline @available(*, deprecated, renamed: "metadataSources.logger") var metadata: Logging.Logger.Metadata { get { metadataSources.logger } set { metadataSources.logger = newValue } } } @usableFromInline let config: Configuration @usableFromInline var loggerInfo: LoggerInfo @inlinable public var logLevel: Logging.Logger.Level { get { loggerInfo.logLevel } set { loggerInfo.logLevel = newValue } } @inlinable public var metadataProvider: Logging.Logger.MetadataProvider? { get { loggerInfo.metadataSources.provider } set { loggerInfo.metadataSources.provider = newValue } } @inlinable public var metadata: Logging.Logger.Metadata { get { loggerInfo.metadataSources.logger } set { loggerInfo.metadataSources.logger = newValue } } @inlinable public subscript(metadataKey metadataKey: String) -> Logging.Logger.Metadata.Value? { get { metadata[metadataKey] } set { metadata[metadataKey] = newValue } } private init(config: Configuration, loggerInfo: LoggerInfo) { self.config = config self.loggerInfo = loggerInfo } /// Returns whether a message with the given level and the given metadata should be logged asynchronous. /// - Parameters: /// - level: The level at which the message was logged. /// - metadata: The metadata associated with the message. /// - Returns: Whether to log the message asynchronous. @usableFromInline func _logAsync(level: Logging.Logger.Level, metadata: Logging.Logger.Metadata?) -> Bool { if level >= config.syncLogging.tresholdLevel { // Easiest check -> level is above treshold. Not async. return false } else if case .stringConvertible(let logSynchronous as Bool) = metadata?[config.syncLogging.metadataKey] { // There's a metadata value, return it's value. We need to invert it since it defines whether to log _synchronous_. return !logSynchronous } else { // If we're below the treshold and no metadata value is set -> we're logging async. return true } } @inlinable public func log(level: Logging.Logger.Level, message: Logging.Logger.Message, metadata: Logging.Logger.Metadata?, source: String, file: String, function: String, line: UInt) { let slMessage = SwiftLogMessage(loggerLabel: loggerInfo.label, loggerMetadata: loggerInfo.metadataSources.logger, loggerProvidedMetadata: loggerInfo.metadataSources.provider?.get(), message: message, level: level, metadata: metadata, source: source, file: file, function: function, line: line) config.log.log(asynchronous: _logAsync(level: level, metadata: metadata), message: slMessage) } } #if swift(>=5.6) /// A typealias for the "old" log handler factory. public typealias OldLogHandlerFactory = (String) -> any LogHandler /// A typealias for the log handler factory. public typealias LogHandlerFactory = (String, Logging.Logger.MetadataProvider?) -> any LogHandler #else /// A typealias for the "old" log handler factory. public typealias OldLogHandlerFactory = (String) -> LogHandler /// A typealias for the log handler factory. public typealias LogHandlerFactory = (String, Logging.Logger.MetadataProvider?) -> LogHandler #endif extension DDLogHandler { /// The default key to control per message whether to log it synchronous or asynchronous. public static var defaultSynchronousLoggingMetadataKey: Logging.Logger.Metadata.Key { "log-synchronous" } /// Creates a new ``LogHandler`` factory using ``DDLogHandler`` with the given parameters. /// - Parameters: /// - log: The ``DDLog`` instance to use for logging. Defaults to ``DDLog/sharedInstance``. /// - defaultLogLevel: The default log level for new loggers. Defaults to ``Logging/Logger/Level/info``. /// - syncLoggingTreshold: The level as of which log messages should be logged synchronously instead of asynchronously. Defaults to ``Logging/Logger/Level/error``. /// - synchronousLoggingMetadataKey: The metadata key to check on messages to decide whether to log synchronous or asynchronous. Defaults to ``DDLogHandler/defaultSynchronousLoggingMetadataKey``. /// - Returns: A new ``LogHandler`` factory using ``DDLogHandler`` that can be passed to ``LoggingSystem/bootstrap``. /// - SeeAlso: ``DDLog``, ``LoggingSystem/boostrap`` public static func handlerFactory( for log: DDLog = .sharedInstance, defaultLogLevel: Logging.Logger.Level = .info, loggingSynchronousAsOf syncLoggingTreshold: Logging.Logger.Level = .error, synchronousLoggingMetadataKey: Logging.Logger.Metadata.Key = DDLogHandler.defaultSynchronousLoggingMetadataKey ) -> LogHandlerFactory { let config = DDLogHandler.Configuration( log: log, syncLogging: .init(tresholdLevel: syncLoggingTreshold, metadataKey: synchronousLoggingMetadataKey) ) return { DDLogHandler(config: config, loggerInfo: .init(label: $0, logLevel: defaultLogLevel, metadataSources: .init(provider: $1))) } } /// Creates a new ``LogHandler`` factory using ``DDLogHandler`` with the given parameters. /// - Parameters: /// - log: The ``DDLog`` instance to use for logging. Defaults to ``DDLog/sharedInstance``. /// - defaultLogLevel: The default log level for new loggers. Defaults to ``Logging/Logger/Level/info``. /// - syncLoggingTreshold: The level as of which log messages should be logged synchronously instead of asynchronously. Defaults to ``Logging/Logger/Level/error``. /// - synchronousLoggingMetadataKey: The metadata key to check on messages to decide whether to log synchronous or asynchronous. Defaults to ``DDLogHandler/defaultSynchronousLoggingMetadataKey``. /// - Returns: A new ``LogHandler`` factory using ``DDLogHandler`` that can be passed to ``LoggingSystem/bootstrap``. /// - SeeAlso: ``DDLog``, ``LoggingSystem/boostrap`` @inlinable public static func handlerFactory( for log: DDLog = .sharedInstance, defaultLogLevel: Logging.Logger.Level = .info, loggingSynchronousAsOf syncLoggingTreshold: Logging.Logger.Level = .error, synchronousLoggingMetadataKey: Logging.Logger.Metadata.Key = DDLogHandler.defaultSynchronousLoggingMetadataKey ) -> OldLogHandlerFactory { let factory: LogHandlerFactory = handlerFactory( for: log, defaultLogLevel: defaultLogLevel, loggingSynchronousAsOf: syncLoggingTreshold, synchronousLoggingMetadataKey: synchronousLoggingMetadataKey ) return { factory($0, nil) } } } extension LoggingSystem { /// Bootraps the logging system with a new ``LogHandler`` factory using ``DDLogHandler``. /// - Parameters: /// - log: The ``DDLog`` instance to use for logging. Defaults to ``DDLog/sharedInstance``. /// - defaultLogLevel: The default log level for new loggers. Defaults to ``Logging/Logger/Level/info``. /// - syncLoggingTreshold: The level as of which log messages should be logged synchronously instead of asynchronously. Defaults to ``Logging/Logger/Level/error``. /// - synchronousLoggingMetadataKey: The metadata key to check on messages to decide whether to log synchronous or asynchronous. Defaults to ``DDLogHandler/defaultSynchronousLoggingMetadataKey``. /// - metadataProvider: The (global) metadata provider to use with the setup. Defaults to `nil`. /// - SeeAlso: ``DDLogHandler/handlerFactory``, ``LoggingSystem/bootstrap`` @inlinable public static func bootstrapWithCocoaLumberjack( for log: DDLog = .sharedInstance, defaultLogLevel: Logging.Logger.Level = .info, loggingSynchronousAsOf syncLoggingTreshold: Logging.Logger.Level = .error, synchronousLoggingMetadataKey: Logging.Logger.Metadata.Key = DDLogHandler.defaultSynchronousLoggingMetadataKey, metadataProvider: Logging.Logger.MetadataProvider? = nil ) { bootstrap(DDLogHandler.handlerFactory(for: log, defaultLogLevel: defaultLogLevel, loggingSynchronousAsOf: syncLoggingTreshold, synchronousLoggingMetadataKey: synchronousLoggingMetadataKey), metadataProvider: metadataProvider) } }