DDLogHandler.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. // Software License Agreement (BSD License)
  2. //
  3. // Copyright (c) 2010-2025, Deusty, LLC
  4. // All rights reserved.
  5. //
  6. // Redistribution and use of this software in source and binary forms,
  7. // with or without modification, are permitted provided that the following conditions are met:
  8. //
  9. // * Redistributions of source code must retain the above copyright notice,
  10. // this list of conditions and the following disclaimer.
  11. //
  12. // * Neither the name of Deusty nor the names of its contributors may be used
  13. // to endorse or promote products derived from this software without specific
  14. // prior written permission of Deusty, LLC.
  15. import CocoaLumberjack
  16. import Logging
  17. extension Logging.Logger.Level {
  18. @inlinable
  19. var ddLogLevelAndFlag: (DDLogLevel, DDLogFlag) {
  20. switch self {
  21. case .trace: return (.verbose, .verbose)
  22. case .debug: return (.debug, .debug)
  23. case .info, .notice: return (.info, .info)
  24. case .warning: return (.warning, .warning)
  25. case .error, .critical: return (.error, .error)
  26. }
  27. }
  28. }
  29. extension DDLogMessage {
  30. /// Contains the swift-log details of a given log message.
  31. public struct SwiftLogInformation: Equatable, Sendable {
  32. /// Contains information about the swift-log logger that logged this message.
  33. public struct LoggerInformation: Equatable, Sendable {
  34. /// Contains the metadata from the various sources of on a logger.
  35. /// Currently this can be the logger itself, as well as its metadata provider
  36. public struct MetadataSources: Equatable, Sendable {
  37. /// The metadata of the swift-log logger that logged this message.
  38. public let logger: Logging.Logger.Metadata
  39. /// The metadata of the metadata provider on the swift-log logger that logged this message.
  40. public let provider: Logging.Logger.Metadata?
  41. }
  42. /// The label of the swift-log logger that logged this message.
  43. public let label: String
  44. /// The metadata of the swift-log logger that logged this message.
  45. public let metadataSources: MetadataSources
  46. /// The metadata of the swift-log logger that logged this message.
  47. @available(*, deprecated, renamed: "metadataSources.logger")
  48. public var metadata: Logging.Logger.Metadata { metadataSources.logger }
  49. }
  50. /// Contains information about the swift-log message thas was logged.
  51. public struct MessageInformation: Equatable, Sendable {
  52. /// The original swift-log message.
  53. public let message: Logging.Logger.Message
  54. /// The original swift-log level of the message. This could be more fine-grained than ``DDLogMessage/level`` & ``DDLogMessage/flag``.
  55. public let level: Logging.Logger.Level
  56. /// The original swift-log metadata of the message.
  57. public let metadata: Logging.Logger.Metadata?
  58. /// The original swift-log source of the message.
  59. public let source: String
  60. }
  61. /// The information about the swift-log logger that logged this message.
  62. public let logger: LoggerInformation
  63. /// The information about the swift-log message that was logged.
  64. public let message: MessageInformation
  65. /// Merges the metadata from all layers together.
  66. /// The metadata on the logger provides the base.
  67. /// Metadata from the logger's metadata provider (if any) trumps the base.
  68. /// Metadata from the logged message again trumps both the base and the metadata from the logger's metadata provider.
  69. /// Essentially: `logger.metadata < logger.metadataProvider < message.metadata`
  70. /// - Note: Accessing this property performs the merge! Accessing it multiple times can be a performance issue!
  71. public var mergedMetadata: Logging.Logger.Metadata {
  72. var merged = logger.metadataSources.logger
  73. if let providerMetadata = logger.metadataSources.provider {
  74. merged.merge(providerMetadata, uniquingKeysWith: { $1 })
  75. }
  76. if let messageMetadata = message.metadata {
  77. merged.merge(messageMetadata, uniquingKeysWith: { $1 })
  78. }
  79. return merged
  80. }
  81. }
  82. /// The swift-log information of this log message. This only exists for messages logged via swift-log.
  83. /// - SeeAlso: ``DDLogMessage/SwiftLogInformation``
  84. @inlinable
  85. public var swiftLogInfo: SwiftLogInformation? {
  86. (self as? SwiftLogMessage)?._swiftLogInfo
  87. }
  88. }
  89. /// This class (intentionally internal) is only an "encapsulation" layer above ``DDLogMessage``.
  90. /// It's basically an implementation detail of ``DDLogMessage/swiftLogInfo``.
  91. @usableFromInline
  92. final class SwiftLogMessage: DDLogMessage, @unchecked Sendable {
  93. @usableFromInline
  94. let _swiftLogInfo: SwiftLogInformation
  95. @usableFromInline
  96. init(loggerLabel: String,
  97. loggerMetadata: Logging.Logger.Metadata,
  98. loggerProvidedMetadata: Logging.Logger.Metadata?,
  99. message: Logging.Logger.Message,
  100. level: Logging.Logger.Level,
  101. metadata: Logging.Logger.Metadata?,
  102. source: String,
  103. file: String,
  104. function: String,
  105. line: UInt) {
  106. _swiftLogInfo = .init(logger: .init(label: loggerLabel,
  107. metadataSources: .init(logger: loggerMetadata,
  108. provider: loggerProvidedMetadata)),
  109. message: .init(message: message,
  110. level: level,
  111. metadata: metadata,
  112. source: source))
  113. let (ddLogLevel, ddLogFlag) = level.ddLogLevelAndFlag
  114. let msg = String(describing: message)
  115. super.init(format: msg,
  116. formatted: msg, // We have no chance in retrieving the original format here.
  117. level: ddLogLevel,
  118. flag: ddLogFlag,
  119. context: 0,
  120. file: file,
  121. function: function,
  122. line: line,
  123. tag: nil,
  124. options: .dontCopyMessage, // Swift will bridge to NSString. No need to make an additional copy.
  125. timestamp: nil) // Passing nil will make DDLogMessage create the timestamp which saves us the bridging between Date and NSDate.
  126. }
  127. // Not removed due to `@usableFromInline`.
  128. @usableFromInline
  129. @available(*, deprecated, renamed: "init(loggerLabel:loggerMetadata:loggerMetadata:message:level:metadata:source:file:function:line:)")
  130. convenience init(loggerLabel: String,
  131. loggerMetadata: Logging.Logger.Metadata,
  132. message: Logging.Logger.Message,
  133. level: Logging.Logger.Level,
  134. metadata: Logging.Logger.Metadata?,
  135. source: String,
  136. file: String,
  137. function: String,
  138. line: UInt) {
  139. self.init(loggerLabel: loggerLabel,
  140. loggerMetadata: loggerMetadata,
  141. loggerProvidedMetadata: nil,
  142. message: message,
  143. level: level,
  144. metadata: metadata,
  145. source: source,
  146. file: file,
  147. function: function,
  148. line: line)
  149. }
  150. override func isEqual(_ object: Any?) -> Bool {
  151. super.isEqual(object) && (object as? SwiftLogMessage)?._swiftLogInfo == _swiftLogInfo
  152. }
  153. }
  154. /// A swift-log ``LogHandler`` implementation that forwards messages to a given ``DDLog`` instance.
  155. public struct DDLogHandler: LogHandler {
  156. @usableFromInline
  157. struct Configuration: Sendable {
  158. @usableFromInline
  159. struct SyncLogging: Sendable {
  160. @usableFromInline
  161. let tresholdLevel: Logging.Logger.Level
  162. @usableFromInline
  163. let metadataKey: Logging.Logger.Metadata.Key
  164. }
  165. @usableFromInline
  166. let log: DDLog
  167. @usableFromInline
  168. let syncLogging: SyncLogging
  169. }
  170. @usableFromInline
  171. struct LoggerInfo: Sendable {
  172. @usableFromInline
  173. struct MetadataSources: Sendable {
  174. @usableFromInline
  175. var provider: Logging.Logger.MetadataProvider?
  176. @usableFromInline
  177. var logger: Logging.Logger.Metadata = [:]
  178. }
  179. @usableFromInline
  180. let label: String
  181. @usableFromInline
  182. var logLevel: Logging.Logger.Level
  183. @usableFromInline
  184. var metadataSources: MetadataSources
  185. // Not removed due to `@usableFromInline`
  186. @usableFromInline
  187. @available(*, deprecated, renamed: "metadataSources.logger")
  188. var metadata: Logging.Logger.Metadata {
  189. get { metadataSources.logger }
  190. set { metadataSources.logger = newValue }
  191. }
  192. }
  193. @usableFromInline
  194. let config: Configuration
  195. @usableFromInline
  196. var loggerInfo: LoggerInfo
  197. @inlinable
  198. public var logLevel: Logging.Logger.Level {
  199. get { loggerInfo.logLevel }
  200. set { loggerInfo.logLevel = newValue }
  201. }
  202. @inlinable
  203. public var metadataProvider: Logging.Logger.MetadataProvider? {
  204. get { loggerInfo.metadataSources.provider }
  205. set { loggerInfo.metadataSources.provider = newValue }
  206. }
  207. @inlinable
  208. public var metadata: Logging.Logger.Metadata {
  209. get { loggerInfo.metadataSources.logger }
  210. set { loggerInfo.metadataSources.logger = newValue }
  211. }
  212. @inlinable
  213. public subscript(metadataKey metadataKey: String) -> Logging.Logger.Metadata.Value? {
  214. get { metadata[metadataKey] }
  215. set { metadata[metadataKey] = newValue }
  216. }
  217. private init(config: Configuration, loggerInfo: LoggerInfo) {
  218. self.config = config
  219. self.loggerInfo = loggerInfo
  220. }
  221. /// Returns whether a message with the given level and the given metadata should be logged asynchronous.
  222. /// - Parameters:
  223. /// - level: The level at which the message was logged.
  224. /// - metadata: The metadata associated with the message.
  225. /// - Returns: Whether to log the message asynchronous.
  226. @usableFromInline
  227. func _logAsync(level: Logging.Logger.Level, metadata: Logging.Logger.Metadata?) -> Bool {
  228. if level >= config.syncLogging.tresholdLevel {
  229. // Easiest check -> level is above treshold. Not async.
  230. return false
  231. } else if case .stringConvertible(let logSynchronous as Bool) = metadata?[config.syncLogging.metadataKey] {
  232. // There's a metadata value, return it's value. We need to invert it since it defines whether to log _synchronous_.
  233. return !logSynchronous
  234. } else {
  235. // If we're below the treshold and no metadata value is set -> we're logging async.
  236. return true
  237. }
  238. }
  239. @inlinable
  240. public func log(level: Logging.Logger.Level,
  241. message: Logging.Logger.Message,
  242. metadata: Logging.Logger.Metadata?,
  243. source: String,
  244. file: String,
  245. function: String,
  246. line: UInt) {
  247. let slMessage = SwiftLogMessage(loggerLabel: loggerInfo.label,
  248. loggerMetadata: loggerInfo.metadataSources.logger,
  249. loggerProvidedMetadata: loggerInfo.metadataSources.provider?.get(),
  250. message: message,
  251. level: level,
  252. metadata: metadata,
  253. source: source,
  254. file: file,
  255. function: function,
  256. line: line)
  257. config.log.log(asynchronous: _logAsync(level: level, metadata: metadata), message: slMessage)
  258. }
  259. }
  260. #if swift(>=5.6)
  261. /// A typealias for the "old" log handler factory.
  262. public typealias OldLogHandlerFactory = (String) -> any LogHandler
  263. /// A typealias for the log handler factory.
  264. public typealias LogHandlerFactory = (String, Logging.Logger.MetadataProvider?) -> any LogHandler
  265. #else
  266. /// A typealias for the "old" log handler factory.
  267. public typealias OldLogHandlerFactory = (String) -> LogHandler
  268. /// A typealias for the log handler factory.
  269. public typealias LogHandlerFactory = (String, Logging.Logger.MetadataProvider?) -> LogHandler
  270. #endif
  271. extension DDLogHandler {
  272. /// The default key to control per message whether to log it synchronous or asynchronous.
  273. public static var defaultSynchronousLoggingMetadataKey: Logging.Logger.Metadata.Key {
  274. "log-synchronous"
  275. }
  276. /// Creates a new ``LogHandler`` factory using ``DDLogHandler`` with the given parameters.
  277. /// - Parameters:
  278. /// - log: The ``DDLog`` instance to use for logging. Defaults to ``DDLog/sharedInstance``.
  279. /// - defaultLogLevel: The default log level for new loggers. Defaults to ``Logging/Logger/Level/info``.
  280. /// - syncLoggingTreshold: The level as of which log messages should be logged synchronously instead of asynchronously. Defaults to ``Logging/Logger/Level/error``.
  281. /// - synchronousLoggingMetadataKey: The metadata key to check on messages to decide whether to log synchronous or asynchronous. Defaults to ``DDLogHandler/defaultSynchronousLoggingMetadataKey``.
  282. /// - Returns: A new ``LogHandler`` factory using ``DDLogHandler`` that can be passed to ``LoggingSystem/bootstrap``.
  283. /// - SeeAlso: ``DDLog``, ``LoggingSystem/boostrap``
  284. public static func handlerFactory(
  285. for log: DDLog = .sharedInstance,
  286. defaultLogLevel: Logging.Logger.Level = .info,
  287. loggingSynchronousAsOf syncLoggingTreshold: Logging.Logger.Level = .error,
  288. synchronousLoggingMetadataKey: Logging.Logger.Metadata.Key = DDLogHandler.defaultSynchronousLoggingMetadataKey
  289. ) -> LogHandlerFactory {
  290. let config = DDLogHandler.Configuration(
  291. log: log,
  292. syncLogging: .init(tresholdLevel: syncLoggingTreshold,
  293. metadataKey: synchronousLoggingMetadataKey)
  294. )
  295. return {
  296. DDLogHandler(config: config, loggerInfo: .init(label: $0, logLevel: defaultLogLevel, metadataSources: .init(provider: $1)))
  297. }
  298. }
  299. /// Creates a new ``LogHandler`` factory using ``DDLogHandler`` with the given parameters.
  300. /// - Parameters:
  301. /// - log: The ``DDLog`` instance to use for logging. Defaults to ``DDLog/sharedInstance``.
  302. /// - defaultLogLevel: The default log level for new loggers. Defaults to ``Logging/Logger/Level/info``.
  303. /// - syncLoggingTreshold: The level as of which log messages should be logged synchronously instead of asynchronously. Defaults to ``Logging/Logger/Level/error``.
  304. /// - synchronousLoggingMetadataKey: The metadata key to check on messages to decide whether to log synchronous or asynchronous. Defaults to ``DDLogHandler/defaultSynchronousLoggingMetadataKey``.
  305. /// - Returns: A new ``LogHandler`` factory using ``DDLogHandler`` that can be passed to ``LoggingSystem/bootstrap``.
  306. /// - SeeAlso: ``DDLog``, ``LoggingSystem/boostrap``
  307. @inlinable
  308. public static func handlerFactory(
  309. for log: DDLog = .sharedInstance,
  310. defaultLogLevel: Logging.Logger.Level = .info,
  311. loggingSynchronousAsOf syncLoggingTreshold: Logging.Logger.Level = .error,
  312. synchronousLoggingMetadataKey: Logging.Logger.Metadata.Key = DDLogHandler.defaultSynchronousLoggingMetadataKey
  313. ) -> OldLogHandlerFactory {
  314. let factory: LogHandlerFactory = handlerFactory(
  315. for: log,
  316. defaultLogLevel: defaultLogLevel,
  317. loggingSynchronousAsOf: syncLoggingTreshold,
  318. synchronousLoggingMetadataKey: synchronousLoggingMetadataKey
  319. )
  320. return { factory($0, nil) }
  321. }
  322. }
  323. extension LoggingSystem {
  324. /// Bootraps the logging system with a new ``LogHandler`` factory using ``DDLogHandler``.
  325. /// - Parameters:
  326. /// - log: The ``DDLog`` instance to use for logging. Defaults to ``DDLog/sharedInstance``.
  327. /// - defaultLogLevel: The default log level for new loggers. Defaults to ``Logging/Logger/Level/info``.
  328. /// - syncLoggingTreshold: The level as of which log messages should be logged synchronously instead of asynchronously. Defaults to ``Logging/Logger/Level/error``.
  329. /// - synchronousLoggingMetadataKey: The metadata key to check on messages to decide whether to log synchronous or asynchronous. Defaults to ``DDLogHandler/defaultSynchronousLoggingMetadataKey``.
  330. /// - metadataProvider: The (global) metadata provider to use with the setup. Defaults to `nil`.
  331. /// - SeeAlso: ``DDLogHandler/handlerFactory``, ``LoggingSystem/bootstrap``
  332. @inlinable
  333. public static func bootstrapWithCocoaLumberjack(
  334. for log: DDLog = .sharedInstance,
  335. defaultLogLevel: Logging.Logger.Level = .info,
  336. loggingSynchronousAsOf syncLoggingTreshold: Logging.Logger.Level = .error,
  337. synchronousLoggingMetadataKey: Logging.Logger.Metadata.Key = DDLogHandler.defaultSynchronousLoggingMetadataKey,
  338. metadataProvider: Logging.Logger.MetadataProvider? = nil
  339. ) {
  340. bootstrap(DDLogHandler.handlerFactory(for: log,
  341. defaultLogLevel: defaultLogLevel,
  342. loggingSynchronousAsOf: syncLoggingTreshold,
  343. synchronousLoggingMetadataKey: synchronousLoggingMetadataKey),
  344. metadataProvider: metadataProvider)
  345. }
  346. }