diff --git a/Package.swift b/Package.swift index c58648b..ce0dc73 100644 --- a/Package.swift +++ b/Package.swift @@ -4,25 +4,24 @@ import PackageDescription let package = Package( - name: "DLog", - platforms: [ - .iOS(.v12), - .macOS(.v10_14), - .tvOS(.v12), - .watchOS(.v5) - ], - products: [ - .library(name: "DLog", targets: ["DLog"]), - .library(name: "DLogObjC", targets: ["DLogObjC"]), - .executable(name: "NetConsole", targets: ["NetConsole"]) - ], - targets: [ - .target(name: "DLog"), - .target(name: "DLogObjC", dependencies: ["DLog"]), - .target(name: "NetConsole"), - .testTarget(name: "DLogTests", dependencies: ["DLog"]), - .testTarget(name: "DLogTestsObjC", dependencies: ["DLogObjC"]), - ], - swiftLanguageVersions: [.v5] + name: "DLog", + platforms: [ + .iOS(.v12), + .macOS(.v10_14), + .tvOS(.v12), + .watchOS(.v5) + ], + products: [ + .library(name: "DLog", targets: ["DLog"]), + .library(name: "DLogObjC", targets: ["DLogObjC"]), + .executable(name: "NetConsole", targets: ["NetConsole"]) + ], + targets: [ + .target(name: "DLog"), + .target(name: "DLogObjC", dependencies: ["DLog"]), + .target(name: "NetConsole"), + .testTarget(name: "DLogTests", dependencies: ["DLog"]), + .testTarget(name: "DLogTestsObjC", dependencies: ["DLogObjC"]), + ], + swiftLanguageVersions: [.v5] ) - diff --git a/Sources/DLog/Atomic.swift b/Sources/DLog/Atomic.swift index a728d5a..78895ab 100644 --- a/Sources/DLog/Atomic.swift +++ b/Sources/DLog/Atomic.swift @@ -27,27 +27,27 @@ import Foundation @discardableResult func synchronized(_ obj: T, closure: () -> U) -> U { - objc_sync_enter(obj) - defer { - objc_sync_exit(obj) - } - return closure() + objc_sync_enter(obj) + defer { + objc_sync_exit(obj) + } + return closure() } @propertyWrapper public class Atomic { - private var value: T - - public init(wrappedValue value: T) { - self.value = value - } - - public var wrappedValue: T { - get { - synchronized(self) { value } - } - set { - synchronized(self) { value = newValue } - } - } + private var value: T + + public init(wrappedValue value: T) { + self.value = value + } + + public var wrappedValue: T { + get { + synchronized(self) { value } + } + set { + synchronized(self) { value = newValue } + } + } } diff --git a/Sources/DLog/DLog.swift b/Sources/DLog/DLog.swift index d71dcaa..644b12c 100644 --- a/Sources/DLog/DLog.swift +++ b/Sources/DLog/DLog.swift @@ -29,135 +29,135 @@ import Foundation /// The central class to emit log messages to specified outputs using one of the methods corresponding to a log level. /// public class DLog: LogProtocol { - - private let output: LogOutput? - - /// The shared disabled logger. - /// - /// Using this constant prevents from logging messages. - /// - /// let logger = DLog.disabled - /// - @objc - public static let disabled = DLog(nil) - - /// Creates a logger object that assigns log messages to a specified category. - /// - /// You can define category name to differentiate unique areas and parts of your app and DLog uses this value - /// to categorize and filter related log messages. - /// - /// let logger = DLog() - /// let netLogger = logger["NET"] - /// let netLogger.log("Hello Net!") - /// - /// - Parameters: - /// - name: Name of category. - @objc - public subscript(name: String) -> LogProtocol { - category(name: name) - } + + private let output: LogOutput? + + /// The shared disabled logger. + /// + /// Using this constant prevents from logging messages. + /// + /// let logger = DLog.disabled + /// + @objc + public static let disabled = DLog(nil) + + /// Creates a logger object that assigns log messages to a specified category. + /// + /// You can define category name to differentiate unique areas and parts of your app and DLog uses this value + /// to categorize and filter related log messages. + /// + /// let logger = DLog() + /// let netLogger = logger["NET"] + /// let netLogger.log("Hello Net!") + /// + /// - Parameters: + /// - name: Name of category. + @objc + public subscript(name: String) -> LogProtocol { + category(name: name) + } + + /// Creates a logger object with a configuration that assigns log messages to a specified category. + /// + /// You can define category name to differentiate unique areas and parts of your app and DLog uses this value + /// to categorize and filter related log messages. + /// + /// var config = LogConfig() + /// config.sign = ">" + /// + /// let logger = DLog() + /// let netLogger = logger.category(name: "NET", config: config) + /// + /// - Parameters: + /// - name: Name of category. + /// - config: Configuration of category. + public func category(name: String, config: LogConfig? = nil, metadata: Metadata? = nil) -> LogProtocol { + LogProtocol(logger: self, category: name, config: config ?? self.config, metadata: metadata ?? self.metadata.data) + } + + /// Creates the logger instance with a target output object. + /// + /// Create an instance and use it to log text messages about your app’s behaviour and to help you assess the state + /// of your app later. You also can choose a target output and a log level to indicate the severity of that message. + /// + /// let logger = DLog() + /// logger.log("Hello DLog!") + /// + /// - Parameters: + /// - output: A target output object. If it is omitted the logger uses `stdout` by default. + /// + public init(_ output: LogOutput? = .stdout, config: LogConfig = LogConfig(), metadata: Metadata = Metadata()) { + self.output = output + super.init(logger: nil, category: "DLOG", config: config, metadata: metadata) + self.logger = self + } + + /// Creates the logger instance with a list of linked outputs for both swift and objective-c code. + /// + /// Swift: + /// + /// let logger = DLog([.textPlain, .stdout]) + /// + /// Objective-C: + /// + /// DLog* logger = [[DLog alloc] initWithOutputs:@[LogOutput.textPlain, filter, LogOutput.stdOut]]; + /// + /// - Parameters: + /// - outputs: An array of outputs. + /// + @objc + public convenience init(outputs: [LogOutput]) { + var output: LogOutput? - /// Creates a logger object with a configuration that assigns log messages to a specified category. - /// - /// You can define category name to differentiate unique areas and parts of your app and DLog uses this value - /// to categorize and filter related log messages. - /// - /// var config = LogConfig() - /// config.sign = ">" - /// - /// let logger = DLog() - /// let netLogger = logger.category(name: "NET", config: config) - /// - /// - Parameters: - /// - name: Name of category. - /// - config: Configuration of category. - public func category(name: String, config: LogConfig? = nil, metadata: Metadata? = nil) -> LogProtocol { - LogProtocol(logger: self, category: name, config: config ?? self.config, metadata: metadata ?? self.metadata.data) + if outputs.count == 0 { + output = .stdout } - - /// Creates the logger instance with a target output object. - /// - /// Create an instance and use it to log text messages about your app’s behaviour and to help you assess the state - /// of your app later. You also can choose a target output and a log level to indicate the severity of that message. - /// - /// let logger = DLog() - /// logger.log("Hello DLog!") - /// - /// - Parameters: - /// - output: A target output object. If it is omitted the logger uses `stdout` by default. - /// - public init(_ output: LogOutput? = .stdout, config: LogConfig = LogConfig(), metadata: Metadata = Metadata()) { - self.output = output - super.init(logger: nil, category: "DLOG", config: config, metadata: metadata) - self.logger = self - } - - /// Creates the logger instance with a list of linked outputs for both swift and objective-c code. - /// - /// Swift: - /// - /// let logger = DLog([.textPlain, .stdout]) - /// - /// Objective-C: - /// - /// DLog* logger = [[DLog alloc] initWithOutputs:@[LogOutput.textPlain, filter, LogOutput.stdOut]]; - /// - /// - Parameters: - /// - outputs: An array of outputs. - /// - @objc - public convenience init(outputs: [LogOutput]) { - var output: LogOutput? - - if outputs.count == 0 { - output = .stdout - } - else { - output = outputs.count == 1 - ? outputs.first - : outputs.reduce(.textPlain, =>) - } - - self.init(output) + else { + output = outputs.count == 1 + ? outputs.first + : outputs.reduce(.textPlain, =>) } - /// Creates the default logger. - @objc - public convenience init() { - self.init(_:config:metadata:)() - } - - // Scope - - func enter(scope: LogScope) { - guard let out = output else { return } + self.init(output) + } + + /// Creates the default logger. + @objc + public convenience init() { + self.init(_:config:metadata:)() + } + + // Scope + + func enter(scope: LogScope) { + guard let out = output else { return } ScopeStack.shared.append(scope) { out.scopeEnter(scope: scope) } - } - - func leave(scope: LogScope) { - guard let out = output else { return } + } + + func leave(scope: LogScope) { + guard let out = output else { return } ScopeStack.shared.remove(scope) { out.scopeLeave(scope: scope) } - } - - // Interval - - func begin(interval: LogInterval) { - guard let out = output else { return } - out.intervalBegin(interval: interval) - } - - func end(interval: LogInterval) { - guard let out = output else { return } - out.intervalEnd(interval: interval) - } - - func log(message: @escaping () -> LogMessage, type: LogType, category: String, config: LogConfig, scope: LogScope?, metadata: Metadata, file: String, function: String, line: UInt) -> String? { - guard let out = output else { return nil } - let item = LogItem(type: type, category: category, config: config, scope: scope, metadata: metadata, file: file, funcName: function, line: line, message: message) - return out.log(item: item) - } + } + + // Interval + + func begin(interval: LogInterval) { + guard let out = output else { return } + out.intervalBegin(interval: interval) + } + + func end(interval: LogInterval) { + guard let out = output else { return } + out.intervalEnd(interval: interval) + } + + func log(message: @escaping () -> LogMessage, type: LogType, category: String, config: LogConfig, scope: LogScope?, metadata: Metadata, file: String, function: String, line: UInt) -> String? { + guard let out = output else { return nil } + let item = LogItem(type: type, category: category, config: config, scope: scope, metadata: metadata, file: file, funcName: function, line: line, message: message) + return out.log(item: item) + } } diff --git a/Sources/DLog/Dynamic.swift b/Sources/DLog/Dynamic.swift index a608213..b739f34 100644 --- a/Sources/DLog/Dynamic.swift +++ b/Sources/DLog/Dynamic.swift @@ -28,25 +28,25 @@ import os typealias Swift_Demangle = @convention(c) (_ mangledName: UnsafePointer?, - _ mangledNameLength: Int, - _ outputBuffer: UnsafeMutablePointer?, - _ outputBufferSize: UnsafeMutablePointer?, - _ flags: UInt32) -> UnsafeMutablePointer? + _ mangledNameLength: Int, + _ outputBuffer: UnsafeMutablePointer?, + _ outputBufferSize: UnsafeMutablePointer?, + _ flags: UInt32) -> UnsafeMutablePointer? /// Dynamic shared object class Dynamic { - - // Constants - static let dso = UnsafeMutableRawPointer(mutating: #dsohandle) - private static let RTLD_DEFAULT = UnsafeMutableRawPointer(bitPattern: -2) - - private static func dynamic(symbol: String) -> T? { - guard let sym = dlsym(RTLD_DEFAULT, symbol) else { - return nil - } - return unsafeBitCast(sym, to: T.self) + + // Constants + static let dso = UnsafeMutableRawPointer(mutating: #dsohandle) + private static let RTLD_DEFAULT = UnsafeMutableRawPointer(bitPattern: -2) + + private static func dynamic(symbol: String) -> T? { + guard let sym = dlsym(RTLD_DEFAULT, symbol) else { + return nil } - - // Functions - static let OS_ACTIVITY_CURRENT: os_activity_t? = dynamic(symbol: "_os_activity_current") - static let swift_demangle: Swift_Demangle? = dynamic(symbol: "swift_demangle") + return unsafeBitCast(sym, to: T.self) + } + + // Functions + static let OS_ACTIVITY_CURRENT: os_activity_t? = dynamic(symbol: "_os_activity_current") + static let swift_demangle: Swift_Demangle? = dynamic(symbol: "swift_demangle") } diff --git a/Sources/DLog/File.swift b/Sources/DLog/File.swift index 2ea8807..e991873 100644 --- a/Sources/DLog/File.swift +++ b/Sources/DLog/File.swift @@ -29,70 +29,70 @@ import Foundation /// Target output for a file. /// public class File : LogOutput { - private let file: FileHandle? - private let queue = DispatchQueue(label: "File") - - /// Initializes and returns the target file output object associated with the specified file. - /// - /// You can use the file output to write text messages to a file by a provided path. - /// - /// let file = File(path: "/users/user/dlog.txt") - /// let logger = DLog(file) - /// logger.info("It's a file") - /// - /// - Parameters: - /// - path: The path to the file to access. - /// - append: `true` if the file output object should append log messages to the end of an existing file or `false` if you want to clear one. - /// - source: A source output object, if it is omitted, the file output takes `Text` plain output as a source output. - /// - public init(path: String, append: Bool = false, source: LogOutput = .textPlain) { - let fileManager = FileManager.default - if append == false { - try? fileManager.removeItem(atPath: path) - } - - if fileManager.fileExists(atPath: path) == false { - let dir = NSString(string: path).deletingLastPathComponent - try? fileManager.createDirectory(atPath: dir, withIntermediateDirectories: true, attributes: nil) - - fileManager.createFile(atPath: path, contents: nil, attributes: nil) - } - - file = FileHandle(forWritingAtPath: path) - - if append { - file?.seekToEndOfFile() - } - - super.init(source: source) - } - - private func write(_ text: String?) -> String? { - if let str = text, !str.isEmpty { - queue.async { - if let data = (str + "\n").data(using: .utf8) { - self.file?.write(data) - } - } - } - return text - } - - // MARK: - LogOutput - - override func log(item: LogItem) -> String? { - write(super.log(item: item)) - } - - override func scopeEnter(scope: LogScope) -> String? { - write(super.scopeEnter(scope: scope)) - } - - override func scopeLeave(scope: LogScope) -> String? { - write(super.scopeLeave(scope: scope)) - } - - override func intervalEnd(interval: LogInterval) -> String? { - write(super.intervalEnd(interval: interval)) - } + private let file: FileHandle? + private let queue = DispatchQueue(label: "File") + + /// Initializes and returns the target file output object associated with the specified file. + /// + /// You can use the file output to write text messages to a file by a provided path. + /// + /// let file = File(path: "/users/user/dlog.txt") + /// let logger = DLog(file) + /// logger.info("It's a file") + /// + /// - Parameters: + /// - path: The path to the file to access. + /// - append: `true` if the file output object should append log messages to the end of an existing file or `false` if you want to clear one. + /// - source: A source output object, if it is omitted, the file output takes `Text` plain output as a source output. + /// + public init(path: String, append: Bool = false, source: LogOutput = .textPlain) { + let fileManager = FileManager.default + if append == false { + try? fileManager.removeItem(atPath: path) + } + + if fileManager.fileExists(atPath: path) == false { + let dir = NSString(string: path).deletingLastPathComponent + try? fileManager.createDirectory(atPath: dir, withIntermediateDirectories: true, attributes: nil) + + fileManager.createFile(atPath: path, contents: nil, attributes: nil) + } + + file = FileHandle(forWritingAtPath: path) + + if append { + file?.seekToEndOfFile() + } + + super.init(source: source) + } + + private func write(_ text: String?) -> String? { + if let str = text, !str.isEmpty { + queue.async { + if let data = (str + "\n").data(using: .utf8) { + self.file?.write(data) + } + } + } + return text + } + + // MARK: - LogOutput + + override func log(item: LogItem) -> String? { + write(super.log(item: item)) + } + + override func scopeEnter(scope: LogScope) -> String? { + write(super.scopeEnter(scope: scope)) + } + + override func scopeLeave(scope: LogScope) -> String? { + write(super.scopeLeave(scope: scope)) + } + + override func intervalEnd(interval: LogInterval) -> String? { + write(super.intervalEnd(interval: interval)) + } } diff --git a/Sources/DLog/Filter.swift b/Sources/DLog/Filter.swift index e13e305..0e509b4 100644 --- a/Sources/DLog/Filter.swift +++ b/Sources/DLog/Filter.swift @@ -28,48 +28,48 @@ import Foundation /// Middleware output for filtering /// public class Filter: LogOutput { - private let isItem: ((LogItem) -> Bool)? - private let isScope: ((LogScope) -> Bool)? - - /// Initializes a filter output that evaluates using a specified block object. - /// - /// Represents a pipe middleware output that can filter log messages by available fields of an evaluated object. - /// - /// // Logs debug messages only - /// let logger = DLog(.textPlain => .filter { $0.type == .debug } => .stdout) - /// - /// - Parameters: - /// - block: The block is applied to the object to be evaluated. - /// - public init(isItem: ((LogItem) -> Bool)?, isScope: ((LogScope) -> Bool)?) { - self.isItem = isItem - self.isScope = isScope - super.init(source: nil) - } - - // MARK: - LogOutput - - override func log(item: LogItem) -> String? { - let text = super.log(item: item) - let included = isItem == nil || isItem?(item) == true - return included ? text : nil - } - - override func scopeEnter(scope: LogScope) -> String? { - let text = super.scopeEnter(scope: scope) - let included = isScope == nil || isScope?(scope) == true - return included == true ? text : nil - } - - override func scopeLeave(scope: LogScope) -> String? { - let text = super.scopeLeave(scope: scope) - let included = isScope == nil || isScope?(scope) == true - return included ? text : nil - } - - override func intervalEnd(interval: LogInterval) -> String? { - let text = super.intervalEnd(interval: interval) - let included = isItem == nil || isItem?(interval) == true - return included ? text : nil - } + private let isItem: ((LogItem) -> Bool)? + private let isScope: ((LogScope) -> Bool)? + + /// Initializes a filter output that evaluates using a specified block object. + /// + /// Represents a pipe middleware output that can filter log messages by available fields of an evaluated object. + /// + /// // Logs debug messages only + /// let logger = DLog(.textPlain => .filter { $0.type == .debug } => .stdout) + /// + /// - Parameters: + /// - block: The block is applied to the object to be evaluated. + /// + public init(isItem: ((LogItem) -> Bool)?, isScope: ((LogScope) -> Bool)?) { + self.isItem = isItem + self.isScope = isScope + super.init(source: nil) + } + + // MARK: - LogOutput + + override func log(item: LogItem) -> String? { + let text = super.log(item: item) + let included = isItem == nil || isItem?(item) == true + return included ? text : nil + } + + override func scopeEnter(scope: LogScope) -> String? { + let text = super.scopeEnter(scope: scope) + let included = isScope == nil || isScope?(scope) == true + return included == true ? text : nil + } + + override func scopeLeave(scope: LogScope) -> String? { + let text = super.scopeLeave(scope: scope) + let included = isScope == nil || isScope?(scope) == true + return included ? text : nil + } + + override func intervalEnd(interval: LogInterval) -> String? { + let text = super.intervalEnd(interval: interval) + let included = isItem == nil || isItem?(interval) == true + return included ? text : nil + } } diff --git a/Sources/DLog/LogConfig.swift b/Sources/DLog/LogConfig.swift index 3fc70bc..045afe2 100644 --- a/Sources/DLog/LogConfig.swift +++ b/Sources/DLog/LogConfig.swift @@ -26,246 +26,246 @@ import Foundation extension OptionSet where RawValue == Int { - /// All available options - public static var all: Self { - Self.init(rawValue: Int.max) - } - - init(_ shift: Int) { - self.init(rawValue: 1 << shift) - } + /// All available options + public static var all: Self { + Self.init(rawValue: Int.max) + } + + init(_ shift: Int) { + self.init(rawValue: 1 << shift) + } } // MARK: - TraceConfig /// Indicates which info from threads should be used. public struct ThreadOptions: OptionSet { - /// The corresponding value of the raw type. - public let rawValue: Int - - /// Creates a new option set from the given raw value. - public init(rawValue: Int) { - self.rawValue = rawValue - } - - /// Number - public static let number = Self(0) - - /// Name (if it exists) - public static let name = Self(1) - - /// Priority - public static let priority = Self(2) - - /// QoS - public static let qos = Self(3) - - /// Stack size - public static let stackSize = Self(4) - - /// Compact: `.number` and `.name` - public static let compact: Self = [.number, .name] - - /// Regular: `.number`, `.name` and `.qos` - public static let regular: Self = [.number, .name, .qos] + /// The corresponding value of the raw type. + public let rawValue: Int + + /// Creates a new option set from the given raw value. + public init(rawValue: Int) { + self.rawValue = rawValue + } + + /// Number + public static let number = Self(0) + + /// Name (if it exists) + public static let name = Self(1) + + /// Priority + public static let priority = Self(2) + + /// QoS + public static let qos = Self(3) + + /// Stack size + public static let stackSize = Self(4) + + /// Compact: `.number` and `.name` + public static let compact: Self = [.number, .name] + + /// Regular: `.number`, `.name` and `.qos` + public static let regular: Self = [.number, .name, .qos] } /// Contains configuration values regarding to thread info. public struct ThreadConfig { - - /// Set which info from threads should be used. Default value is `ThreadOptions.compact`. - public var options: ThreadOptions = .compact + + /// Set which info from threads should be used. Default value is `ThreadOptions.compact`. + public var options: ThreadOptions = .compact } /// Indicates which info from stacks should be used. public struct StackOptions: OptionSet { - /// The corresponding value of the raw type. - public let rawValue: Int - - /// Creates a new option set from the given raw value. - public init(rawValue: Int) { - self.rawValue = rawValue - } - - /// Module name - public static let module = Self(0) - - /// Address - public static let address = Self(1) - - /// Stack symbols - public static let symbols = Self(2) - - /// Offset - public static let offset = Self(3) - - /// Frame - public static let frame = Self(4) + /// The corresponding value of the raw type. + public let rawValue: Int + + /// Creates a new option set from the given raw value. + public init(rawValue: Int) { + self.rawValue = rawValue + } + + /// Module name + public static let module = Self(0) + + /// Address + public static let address = Self(1) + + /// Stack symbols + public static let symbols = Self(2) + + /// Offset + public static let offset = Self(3) + + /// Frame + public static let frame = Self(4) } /// Contains configuration values regarding to stack info public struct StackConfig { - /// Set which info from stacks should be used. Default value is `StackOptions.symbols`. - public var options: StackOptions = .symbols - - /// Depth of stack - public var depth = 0 + /// Set which info from stacks should be used. Default value is `StackOptions.symbols`. + public var options: StackOptions = .symbols + + /// Depth of stack + public var depth = 0 } /// Indicates which info from the `trace` method should be used. public struct TraceOptions: OptionSet { - /// The corresponding value of the raw type. - public let rawValue: Int - - /// Creates a new option set from the given raw value. - public init(rawValue: Int) { - self.rawValue = rawValue - } - - /// Thread - public static let thread = Self(0) - - /// Queue - public static let queue = Self(1) - - /// Function - public static let function = Self(2) - - /// Stack - public static let stack = Self(3) - - /// Compact: `.thread` and `.function` - public static let compact: Self = [.thread, .function] - - /// Regular: `.thread`, `.queue` and `.function` - public static let regular: Self = [.thread, .queue, .function] + /// The corresponding value of the raw type. + public let rawValue: Int + + /// Creates a new option set from the given raw value. + public init(rawValue: Int) { + self.rawValue = rawValue + } + + /// Thread + public static let thread = Self(0) + + /// Queue + public static let queue = Self(1) + + /// Function + public static let function = Self(2) + + /// Stack + public static let stack = Self(3) + + /// Compact: `.thread` and `.function` + public static let compact: Self = [.thread, .function] + + /// Regular: `.thread`, `.queue` and `.function` + public static let regular: Self = [.thread, .queue, .function] } /// Trace view style public enum TraceViewStyle { - /// Flat view - case flat - - /// Pretty view - case pretty + /// Flat view + case flat + + /// Pretty view + case pretty } /// Contains configuration values regarding to the `trace` method. public struct TraceConfig { - /// View style - public var style: TraceViewStyle = .flat - - /// Set which info from the `trace` method should be used. Default value is `TraceOptions.compact`. - public var options: TraceOptions = .compact - - /// Configuration of thread info - public var threadConfig = ThreadConfig() - - /// Configuration of stack info - public var stackConfig = StackConfig() + /// View style + public var style: TraceViewStyle = .flat + + /// Set which info from the `trace` method should be used. Default value is `TraceOptions.compact`. + public var options: TraceOptions = .compact + + /// Configuration of thread info + public var threadConfig = ThreadConfig() + + /// Configuration of stack info + public var stackConfig = StackConfig() } // MARK: - IntervalConfig /// Indicates which info from intervals should be used. public struct IntervalOptions: OptionSet { - /// The corresponding value of the raw type. - public let rawValue: Int - - /// Creates a new option set from the given raw value. - public init(rawValue: Int) { - self.rawValue = rawValue - } - - /// Time duration - public static let duration = Self(0) - - /// Number of total calls - public static let count = Self(1) - - /// Total time duration of all calls - public static let total = Self(2) - - /// Minimum time duration - public static let min = Self(3) - - /// Maximum time duration - public static let max = Self(4) - - /// Average time duration∂ß - public static let average = Self(5) - - /// Compact: `.duration` and `.average` - public static let compact: Self = [.duration, .average] - - /// Regular: `.duration`, `.average`, `.count` and `.total` - public static let regular: Self = [.duration, .average, .count, .total] + /// The corresponding value of the raw type. + public let rawValue: Int + + /// Creates a new option set from the given raw value. + public init(rawValue: Int) { + self.rawValue = rawValue + } + + /// Time duration + public static let duration = Self(0) + + /// Number of total calls + public static let count = Self(1) + + /// Total time duration of all calls + public static let total = Self(2) + + /// Minimum time duration + public static let min = Self(3) + + /// Maximum time duration + public static let max = Self(4) + + /// Average time duration∂ß + public static let average = Self(5) + + /// Compact: `.duration` and `.average` + public static let compact: Self = [.duration, .average] + + /// Regular: `.duration`, `.average`, `.count` and `.total` + public static let regular: Self = [.duration, .average, .count, .total] } /// Contains configuration values regarding to intervals. public struct IntervalConfig { - - /// Set which info from the intervals should be used. Default value is `IntervalOptions.compact`. - public var options: IntervalOptions = .compact + + /// Set which info from the intervals should be used. Default value is `IntervalOptions.compact`. + public var options: IntervalOptions = .compact } // MARK: - LogConfig /// Indicates which info from the logger should be used. public struct LogOptions: OptionSet { - /// The corresponding value of the raw type. - public let rawValue: Int - - /// Creates a new option set from the given raw value. - public init(rawValue: Int) { - self.rawValue = rawValue - } - - /// Start sign - public static let sign = Self(0) - - /// Timestamp - public static let time = Self(1) - - /// Level of the current scope - public static let level = Self(2) - - /// Category - public static let category = Self(3) - - /// The current scope padding - public static let padding = Self(4) - - /// Log type - public static let type = Self(5) - - /// Location - public static let location = Self(6) - - /// Metadata - public static let metadata = Self(7) - - /// Compact: `.sign` and `.time` - public static let compact: Self = [.sign, .time] - - /// Regular: `.sign`, `.time`, `.category`, `.padding`, `.type`, `.location` and `.metadata` - public static let regular: Self = [.sign, .time, .category, .padding, .type, .location, .metadata] + /// The corresponding value of the raw type. + public let rawValue: Int + + /// Creates a new option set from the given raw value. + public init(rawValue: Int) { + self.rawValue = rawValue + } + + /// Start sign + public static let sign = Self(0) + + /// Timestamp + public static let time = Self(1) + + /// Level of the current scope + public static let level = Self(2) + + /// Category + public static let category = Self(3) + + /// The current scope padding + public static let padding = Self(4) + + /// Log type + public static let type = Self(5) + + /// Location + public static let location = Self(6) + + /// Metadata + public static let metadata = Self(7) + + /// Compact: `.sign` and `.time` + public static let compact: Self = [.sign, .time] + + /// Regular: `.sign`, `.time`, `.category`, `.padding`, `.type`, `.location` and `.metadata` + public static let regular: Self = [.sign, .time, .category, .padding, .type, .location, .metadata] } /// Contains configuration values regarding to the logger public struct LogConfig { - /// Start sign of the logger - public var sign: Character = "•" - - /// Set which info from the logger should be used. Default value is `LogOptions.regular`. - public var options: LogOptions = .regular - - /// Configuration of the `trace` method - public var traceConfig = TraceConfig() - - /// Configuration of intervals - public var intervalConfig = IntervalConfig() - - /// Creates the logger's default configuration. - public init() {} + /// Start sign of the logger + public var sign: Character = "•" + + /// Set which info from the logger should be used. Default value is `LogOptions.regular`. + public var options: LogOptions = .regular + + /// Configuration of the `trace` method + public var traceConfig = TraceConfig() + + /// Configuration of intervals + public var intervalConfig = IntervalConfig() + + /// Creates the logger's default configuration. + public init() {} } diff --git a/Sources/DLog/LogFormat.swift b/Sources/DLog/LogFormat.swift index cd6fc8a..bfd126b 100644 --- a/Sources/DLog/LogFormat.swift +++ b/Sources/DLog/LogFormat.swift @@ -34,410 +34,410 @@ fileprivate let dateFormatter = DateFormatter() fileprivate let byteCountFormatter = ByteCountFormatter() fileprivate let numberFormatter = NumberFormatter() fileprivate let dateComponentsFormatter: DateComponentsFormatter = { - let formatter = DateComponentsFormatter() - formatter.allowedUnits = [.year, .month, .weekOfMonth, .day, .hour, .minute, .second] - return formatter + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.year, .month, .weekOfMonth, .day, .hour, .minute, .second] + return formatter }() fileprivate func insertMs(time: String, sec: String, ms: String) -> String { - guard let range = time.range(of: sec) else { - return "\(time) 0\(ms)\(sec)" - } - - var text = time - text.insert(contentsOf: ms, at: range.lowerBound) - return text + guard let range = time.range(of: sec) else { + return "\(time) 0\(ms)\(sec)" + } + + var text = time + text.insert(contentsOf: ms, at: range.lowerBound) + return text } func stringFromTimeInterval(_ timeInterval: TimeInterval, unitsStyle: DateComponentsFormatter.UnitsStyle = .abbreviated) -> String { - let time: String = synchronized(dateComponentsFormatter) { - dateComponentsFormatter.unitsStyle = unitsStyle - return dateComponentsFormatter.string(from: timeInterval) ?? "" - } - - guard let fraction = String(format: "%.3f", timeInterval).split(separator: ".").last, fraction != "000" else { - return time - } - let ms = ".\(fraction)" - - switch unitsStyle { + let time: String = synchronized(dateComponentsFormatter) { + dateComponentsFormatter.unitsStyle = unitsStyle + return dateComponentsFormatter.string(from: timeInterval) ?? "" + } + + guard let fraction = String(format: "%.3f", timeInterval).split(separator: ".").last, fraction != "000" else { + return time + } + let ms = ".\(fraction)" + + switch unitsStyle { case .positional: - return "\(time)\(ms)" - + return "\(time)\(ms)" + case .abbreviated: - return insertMs(time: time, sec: "s", ms: ms) - + return insertMs(time: time, sec: "s", ms: ms) + case .short: - return insertMs(time: time, sec: " sec", ms: ms) - + return insertMs(time: time, sec: " sec", ms: ms) + case .full: - return insertMs(time: time, sec: " second", ms: ms) - + return insertMs(time: time, sec: " second", ms: ms) + case .spellOut: - let text: String = synchronized(numberFormatter) { - numberFormatter.numberStyle = .spellOut - let value = Double(fraction)! - return numberFormatter.string(from: NSNumber(value: value))! - } - return "\(time), \(text) milliseconds" - + let text: String = synchronized(numberFormatter) { + numberFormatter.numberStyle = .spellOut + let value = Double(fraction)! + return numberFormatter.string(from: NSNumber(value: value))! + } + return "\(time), \(text) milliseconds" + case .brief: - return insertMs(time: time, sec: "sec", ms: ms) - + return insertMs(time: time, sec: "sec", ms: ms) + @unknown default: - return time - } + return time + } } /// Format options for date. public enum LogDateFormatting { - - /// Displays a date value with the specified parameters. - /// - /// - Parameters: - /// - dateStyle: Format style for date. The default is `none`. - /// - timeStyle: Format style for time. The default is `none`. - /// - locale: The locale for the receiver. The default is `nil`. - case date(dateStyle: DateFormatter.Style = .none, timeStyle: DateFormatter.Style = .none, locale: Locale? = nil) - - /// Displays a date value with the specified format. - /// - /// - Parameters: - /// - format: Custom format string. - case dateCustom(format: String) - - func string(from value: Date) -> String { - synchronized(dateFormatter) { - switch self { - - case let .date(dateStyle, timeStyle, locale): - dateFormatter.dateStyle = dateStyle - dateFormatter.timeStyle = timeStyle - dateFormatter.locale = locale - return dateFormatter.string(from: value) - - case let .dateCustom(format): - dateFormatter.dateFormat = format - return dateFormatter.string(from: value) - } - } + + /// Displays a date value with the specified parameters. + /// + /// - Parameters: + /// - dateStyle: Format style for date. The default is `none`. + /// - timeStyle: Format style for time. The default is `none`. + /// - locale: The locale for the receiver. The default is `nil`. + case date(dateStyle: DateFormatter.Style = .none, timeStyle: DateFormatter.Style = .none, locale: Locale? = nil) + + /// Displays a date value with the specified format. + /// + /// - Parameters: + /// - format: Custom format string. + case dateCustom(format: String) + + func string(from value: Date) -> String { + synchronized(dateFormatter) { + switch self { + + case let .date(dateStyle, timeStyle, locale): + dateFormatter.dateStyle = dateStyle + dateFormatter.timeStyle = timeStyle + dateFormatter.locale = locale + return dateFormatter.string(from: value) + + case let .dateCustom(format): + dateFormatter.dateFormat = format + return dateFormatter.string(from: value) + } } + } } /// Format options for integers. public enum LogIntFormatting { - /// Displays an integer value in binary format. - case binary - - /// Displays an integer value in octal format with the specified parameters. - /// - /// - Parameters: - /// - includePrefix: Pass `true` to add a prefix 0o. The default is `false`. - case octal(includePrefix: Bool = false) - - /// Displays an integer value in octal format. - public static let octal = Self.octal() - - /// Displays an integer value in hexadecimal format with the specified - /// parameters. - /// - /// - Parameters: - /// - includePrefix: Pass `true` to add a prefix 0x. The default is `false`. - /// - uppercase: Pass `true` to use uppercase letters - /// or `false` to use lowercase letters. The default is `false`. - case hex(includePrefix: Bool = false, uppercase: Bool = false) - - /// Displays an integer value in hexadecimal format. - public static let hex = Self.hex() - - /// Displays an integer value in byte count format with the specified parameters. - /// - /// - Parameters: - /// - countStyle: Style of counts. The default is `file`. - /// - allowedUnits: Units to display. The default is `useMB`. - case byteCount(countStyle: ByteCountFormatter.CountStyle = .file, allowedUnits: ByteCountFormatter.Units = .useMB) - - /// Displays an integer value in byte count format. - public static let byteCount = Self.byteCount(allowedUnits: .useAll) - - /// Displays an integer value in number format with the specified parameters. - /// - /// - Parameters: - /// - style: Format style for number. - /// - locale: The locale for the receiver. The default is `nil`. - case number(style: NumberFormatter.Style, locale: Locale? = nil) - - /// Displays an integer value in number format. - public static let number = Self.number(style: .decimal) - - /// Displays a localized string corresponding to a specified HTTP status code. - case httpStatusCode - - /// Displays an integer value (Int32) as IPv4 address. - /// - /// For instance, 0x0100007f would be displayed as 127.0.0.1 - case ipv4Address - - /// Displays a time duration from seconds. - /// - /// - Parameters: - /// - unitsStyle: Constants for specifying how to represent quantities of time. - case time(unitsStyle: DateComponentsFormatter.UnitsStyle) - - /// Displays a time duration from seconds. - public static let time = Self.time(unitsStyle: .abbreviated) - - /// Displays date from seconds since 1970. - /// - /// - Parameters: - /// - dateStyle: Format style for date. The default is `none`. - /// - timeStyle: Format style for time. The default is `none`. - /// - locale: The locale for the receiver. The default is `nil`. - case date(dateStyle: DateFormatter.Style = .none, timeStyle: DateFormatter.Style = .none, locale: Locale? = nil) - - /// Displays date from seconds since 1970. - public static let date = Self.date(dateStyle: .short, timeStyle: .short) - - func string(from value: T) -> String { - switch self { - case .binary: - return String(value, radix: 2) - - case let .octal(includePrefix): - let prefix = includePrefix ? "0o" : "" - let oct = String(value, radix: 8) - return "\(prefix)\(oct)" - - case let .hex(includePrefix, uppercase): - let prefix = includePrefix ? "0x" : "" - let hex = String(value, radix: 16, uppercase: uppercase) - return "\(prefix)\(hex)" - - case let .byteCount(countStyle, allowedUnits): - return synchronized(byteCountFormatter) { - byteCountFormatter.countStyle = countStyle - byteCountFormatter.allowedUnits = allowedUnits - return byteCountFormatter.string(fromByteCount: Int64(value)) - } + /// Displays an integer value in binary format. + case binary + + /// Displays an integer value in octal format with the specified parameters. + /// + /// - Parameters: + /// - includePrefix: Pass `true` to add a prefix 0o. The default is `false`. + case octal(includePrefix: Bool = false) + + /// Displays an integer value in octal format. + public static let octal = Self.octal() + + /// Displays an integer value in hexadecimal format with the specified + /// parameters. + /// + /// - Parameters: + /// - includePrefix: Pass `true` to add a prefix 0x. The default is `false`. + /// - uppercase: Pass `true` to use uppercase letters + /// or `false` to use lowercase letters. The default is `false`. + case hex(includePrefix: Bool = false, uppercase: Bool = false) + + /// Displays an integer value in hexadecimal format. + public static let hex = Self.hex() + + /// Displays an integer value in byte count format with the specified parameters. + /// + /// - Parameters: + /// - countStyle: Style of counts. The default is `file`. + /// - allowedUnits: Units to display. The default is `useMB`. + case byteCount(countStyle: ByteCountFormatter.CountStyle = .file, allowedUnits: ByteCountFormatter.Units = .useMB) + + /// Displays an integer value in byte count format. + public static let byteCount = Self.byteCount(allowedUnits: .useAll) + + /// Displays an integer value in number format with the specified parameters. + /// + /// - Parameters: + /// - style: Format style for number. + /// - locale: The locale for the receiver. The default is `nil`. + case number(style: NumberFormatter.Style, locale: Locale? = nil) + + /// Displays an integer value in number format. + public static let number = Self.number(style: .decimal) + + /// Displays a localized string corresponding to a specified HTTP status code. + case httpStatusCode + + /// Displays an integer value (Int32) as IPv4 address. + /// + /// For instance, 0x0100007f would be displayed as 127.0.0.1 + case ipv4Address + + /// Displays a time duration from seconds. + /// + /// - Parameters: + /// - unitsStyle: Constants for specifying how to represent quantities of time. + case time(unitsStyle: DateComponentsFormatter.UnitsStyle) + + /// Displays a time duration from seconds. + public static let time = Self.time(unitsStyle: .abbreviated) + + /// Displays date from seconds since 1970. + /// + /// - Parameters: + /// - dateStyle: Format style for date. The default is `none`. + /// - timeStyle: Format style for time. The default is `none`. + /// - locale: The locale for the receiver. The default is `nil`. + case date(dateStyle: DateFormatter.Style = .none, timeStyle: DateFormatter.Style = .none, locale: Locale? = nil) + + /// Displays date from seconds since 1970. + public static let date = Self.date(dateStyle: .short, timeStyle: .short) + + func string(from value: T) -> String { + switch self { + case .binary: + return String(value, radix: 2) - case let .number(style, locale): - return synchronized(numberFormatter) { - numberFormatter.locale = locale - numberFormatter.numberStyle = style - return numberFormatter.string(from: NSNumber(value: Int64(value)))! - } - - case .httpStatusCode: - return "HTTP \(value) \(HTTPURLResponse.localizedString(forStatusCode: Int(value)))" - - case .ipv4Address: - guard value >= 0 else { return "" } - let data = withUnsafeBytes(of: UInt32(value)) { Data($0) } - return IPv4Address(data)!.debugDescription - - case let .time(unitsStyle): - return stringFromTimeInterval(Double(value), unitsStyle: unitsStyle) - - case let .date(dateStyle, timeStyle, locale): - let date = Date(timeIntervalSince1970: Double(value)) - return synchronized(dateFormatter) { - dateFormatter.dateStyle = dateStyle - dateFormatter.timeStyle = timeStyle - dateFormatter.locale = locale - return dateFormatter.string(from: date) - } + case let .octal(includePrefix): + let prefix = includePrefix ? "0o" : "" + let oct = String(value, radix: 8) + return "\(prefix)\(oct)" + + case let .hex(includePrefix, uppercase): + let prefix = includePrefix ? "0x" : "" + let hex = String(value, radix: 16, uppercase: uppercase) + return "\(prefix)\(hex)" + + case let .byteCount(countStyle, allowedUnits): + return synchronized(byteCountFormatter) { + byteCountFormatter.countStyle = countStyle + byteCountFormatter.allowedUnits = allowedUnits + return byteCountFormatter.string(fromByteCount: Int64(value)) + } + + case let .number(style, locale): + return synchronized(numberFormatter) { + numberFormatter.locale = locale + numberFormatter.numberStyle = style + return numberFormatter.string(from: NSNumber(value: Int64(value)))! + } + + case .httpStatusCode: + return "HTTP \(value) \(HTTPURLResponse.localizedString(forStatusCode: Int(value)))" + + case .ipv4Address: + guard value >= 0 else { return "" } + let data = withUnsafeBytes(of: UInt32(value)) { Data($0) } + return IPv4Address(data)!.debugDescription + + case let .time(unitsStyle): + return stringFromTimeInterval(Double(value), unitsStyle: unitsStyle) + + case let .date(dateStyle, timeStyle, locale): + let date = Date(timeIntervalSince1970: Double(value)) + return synchronized(dateFormatter) { + dateFormatter.dateStyle = dateStyle + dateFormatter.timeStyle = timeStyle + dateFormatter.locale = locale + return dateFormatter.string(from: date) } } + } } /// Format options for floating-point numbers. public enum LogFloatFormatting { - /// Displays a floating-point value in fprintf's `%f` format with specified precision. - /// - /// - Parameters: - /// - precision: Number of digits to display after the radix point. - case fixed(precision: Int = 0) - - /// Displays a floating-point value in fprintf's `%f` format with default precision. - public static let fixed = Self.fixed() - - /// Displays a floating-point value in hexadecimal format with the specified parameters. - /// - /// - Parameters: - /// - includePrefix: Pass `true` to add a prefix 0x. The default is `false`. - /// - uppercase: Pass `true` to use uppercase letters - /// or `false` to use lowercase letters. The default is `false`. - case hex(includePrefix: Bool = false, uppercase: Bool = false) - - /// Displays a floating-point value in hexadecimal format. - public static let hex = Self.hex() - - /// Displays a floating-point value in fprintf's `%e` format with specified precision. - /// - /// - Parameters: - /// - precision: Number of digits to display after the radix point. - case exponential(precision: Int = 0) - - /// Displays a floating-point value in fprintf's `%e` format. - public static let exponential = Self.exponential() - - /// Displays a floating-point value in fprintf's `%g` format with the - /// specified precision. - /// - /// - Parameters: - /// - precision: Number of digits to display after the radix point. - case hybrid(precision: Int = 0) - - /// Displays a floating-point value in fprintf's `%g` format. - public static let hybrid = Self.hybrid() - - /// Displays a floating-point value in number format with the specified parameters. - /// - /// - Parameters: - /// - style: Format style for number. - /// - locale: The locale for the receiver. The default is `nil`. - case number(style: NumberFormatter.Style, locale: Locale? = nil) - - /// Displays a time duration from seconds. - /// - /// - Parameters: - /// - unitsStyle: Constants for specifying how to represent quantities of time. - case time(unitsStyle: DateComponentsFormatter.UnitsStyle) - - /// Displays a time duration from seconds. - public static let time = Self.time(unitsStyle: .abbreviated) - - /// Displays date from seconds since 1970. - /// - /// - Parameters: - /// - dateStyle: Format style for date. The default is `none`. - /// - timeStyle: Format style for time. The default is `none`. - /// - locale: The locale for the receiver. The default is `nil`. - case date(dateStyle: DateFormatter.Style = .none, timeStyle: DateFormatter.Style = .none, locale: Locale? = nil) - - /// Displays date from seconds since 1970. - public static let date = Self.date(dateStyle: .short, timeStyle: .short) - - /// Displays a floating-point value in number format. - public static let number = Self.number(style: .decimal) - - func string(from value: T) -> String { - let doubleValue = Double(value) + /// Displays a floating-point value in fprintf's `%f` format with specified precision. + /// + /// - Parameters: + /// - precision: Number of digits to display after the radix point. + case fixed(precision: Int = 0) + + /// Displays a floating-point value in fprintf's `%f` format with default precision. + public static let fixed = Self.fixed() + + /// Displays a floating-point value in hexadecimal format with the specified parameters. + /// + /// - Parameters: + /// - includePrefix: Pass `true` to add a prefix 0x. The default is `false`. + /// - uppercase: Pass `true` to use uppercase letters + /// or `false` to use lowercase letters. The default is `false`. + case hex(includePrefix: Bool = false, uppercase: Bool = false) + + /// Displays a floating-point value in hexadecimal format. + public static let hex = Self.hex() + + /// Displays a floating-point value in fprintf's `%e` format with specified precision. + /// + /// - Parameters: + /// - precision: Number of digits to display after the radix point. + case exponential(precision: Int = 0) + + /// Displays a floating-point value in fprintf's `%e` format. + public static let exponential = Self.exponential() + + /// Displays a floating-point value in fprintf's `%g` format with the + /// specified precision. + /// + /// - Parameters: + /// - precision: Number of digits to display after the radix point. + case hybrid(precision: Int = 0) + + /// Displays a floating-point value in fprintf's `%g` format. + public static let hybrid = Self.hybrid() + + /// Displays a floating-point value in number format with the specified parameters. + /// + /// - Parameters: + /// - style: Format style for number. + /// - locale: The locale for the receiver. The default is `nil`. + case number(style: NumberFormatter.Style, locale: Locale? = nil) + + /// Displays a time duration from seconds. + /// + /// - Parameters: + /// - unitsStyle: Constants for specifying how to represent quantities of time. + case time(unitsStyle: DateComponentsFormatter.UnitsStyle) + + /// Displays a time duration from seconds. + public static let time = Self.time(unitsStyle: .abbreviated) + + /// Displays date from seconds since 1970. + /// + /// - Parameters: + /// - dateStyle: Format style for date. The default is `none`. + /// - timeStyle: Format style for time. The default is `none`. + /// - locale: The locale for the receiver. The default is `nil`. + case date(dateStyle: DateFormatter.Style = .none, timeStyle: DateFormatter.Style = .none, locale: Locale? = nil) + + /// Displays date from seconds since 1970. + public static let date = Self.date(dateStyle: .short, timeStyle: .short) + + /// Displays a floating-point value in number format. + public static let number = Self.number(style: .decimal) + + func string(from value: T) -> String { + let doubleValue = Double(value) + + switch self { + case let .fixed(precision): + return precision > 0 + ? String(format: "%.\(precision)f", doubleValue) + : String(format: "%f", doubleValue) - switch self { - case let .fixed(precision): - return precision > 0 - ? String(format: "%.\(precision)f", doubleValue) - : String(format: "%f", doubleValue) - - case let .hex(includePrefix, uppercase): - var text = String(format: "%a", doubleValue).replacingOccurrences(of: "0x", with: "") - if uppercase { - text = text.uppercased() - } - return "\(includePrefix ? "0x" : "")\(text)" - - case let .exponential(precision): - return precision > 0 - ? String(format: "%.\(precision)e", doubleValue) - : String(format: "%e", doubleValue) - - case let .hybrid(precision): - return precision > 0 - ? String(format: "%.\(precision)g", doubleValue) - : String(format: "%g", doubleValue) - - case let .number(style, locale): - return synchronized(numberFormatter) { - numberFormatter.locale = locale - numberFormatter.numberStyle = style - return numberFormatter.string(from: NSNumber(value: doubleValue))! - } - - case let .time(unitsStyle): - return stringFromTimeInterval(doubleValue, unitsStyle: unitsStyle) - - case let .date(dateStyle, timeStyle, locale): - let date = Date(timeIntervalSince1970: doubleValue) - return synchronized(dateFormatter) { - dateFormatter.dateStyle = dateStyle - dateFormatter.timeStyle = timeStyle - dateFormatter.locale = locale - return dateFormatter.string(from: date) - } + case let .hex(includePrefix, uppercase): + var text = String(format: "%a", doubleValue).replacingOccurrences(of: "0x", with: "") + if uppercase { + text = text.uppercased() + } + return "\(includePrefix ? "0x" : "")\(text)" + + case let .exponential(precision): + return precision > 0 + ? String(format: "%.\(precision)e", doubleValue) + : String(format: "%e", doubleValue) + + case let .hybrid(precision): + return precision > 0 + ? String(format: "%.\(precision)g", doubleValue) + : String(format: "%g", doubleValue) + + case let .number(style, locale): + return synchronized(numberFormatter) { + numberFormatter.locale = locale + numberFormatter.numberStyle = style + return numberFormatter.string(from: NSNumber(value: doubleValue))! + } + + case let .time(unitsStyle): + return stringFromTimeInterval(doubleValue, unitsStyle: unitsStyle) + + case let .date(dateStyle, timeStyle, locale): + let date = Date(timeIntervalSince1970: doubleValue) + return synchronized(dateFormatter) { + dateFormatter.dateStyle = dateStyle + dateFormatter.timeStyle = timeStyle + dateFormatter.locale = locale + return dateFormatter.string(from: date) } } + } } /// The formatting options for Boolean values. public enum LogBoolFormatting { - /// Displays a boolean value as 1 or 0. - case binary - - /// Displays a boolean value as yes or no. - case answer - - /// Displays a boolean value as on or off. - case toggle - - func string(from value: Bool) -> String { - switch self { - case .binary: - return value ? "1" : "0" - - case .toggle: - return value ? "on" : "off" - - case .answer: - return value ? "yes" : "no" - } + /// Displays a boolean value as 1 or 0. + case binary + + /// Displays a boolean value as yes or no. + case answer + + /// Displays a boolean value as on or off. + case toggle + + func string(from value: Bool) -> String { + switch self { + case .binary: + return value ? "1" : "0" + + case .toggle: + return value ? "on" : "off" + + case .answer: + return value ? "yes" : "no" } + } } /// The formatting options for Data. public enum LogDataFormatting { - - /// Pretty prints an IPv6 address from data. - case ipv6Address - - /// Pretty prints text from data. - case text - - /// Pretty prints uuid from data. - case uuid - - /// Pretty prints raw bytes from data. - case raw - - func string(from data: Data) -> String { - switch self { - case .ipv6Address: - guard data.count == 16, let ipv6 = IPv6Address(data) else { - return "" - } - return ipv6.debugDescription - - case .text: - return String(data: data, encoding: .utf8) ?? "" - - case .uuid: - guard data.count == 16 else { - return "" - } - return data.withUnsafeBytes { bytes in - let positions = [4, 6, 8, 10] - let chars: [String] = (0.. String { + switch self { + case .ipv6Address: + guard data.count == 16, let ipv6 = IPv6Address(data) else { + return "" + } + return ipv6.debugDescription + + case .text: + return String(data: data, encoding: .utf8) ?? "" + + case .uuid: + guard data.count == 16 else { + return "" + } + return data.withUnsafeBytes { bytes in + let positions = [4, 6, 8, 10] + let chars: [String] = (0.. IntervalStatistics { - get { - synchronized(self) { - if let data = intervals[id] { - return data - } - let data = IntervalStatistics() - intervals[id] = data - return data - } - } - - set { - synchronized(self) { - intervals[id] = newValue - } + static let shared = StatisticsStore() + + private var intervals = [Int : IntervalStatistics]() + + subscript(id: Int) -> IntervalStatistics { + get { + synchronized(self) { + if let data = intervals[id] { + return data } + let data = IntervalStatistics() + intervals[id] = data + return data + } } + + set { + synchronized(self) { + intervals[id] = newValue + } + } + } } /// An object that represents a time interval triggered by the user. @@ -74,106 +74,106 @@ fileprivate class StatisticsStore { /// Interval logs a point of interest in your code as running time statistics for debugging performance. /// public class LogInterval: LogItem { - private let logger: DLog - private let id: Int - private let name: String - @Atomic - private var begun = false + private let logger: DLog + private let id: Int + private let name: String + @Atomic + private var begun = false + + let staticName: StaticString? + + // SignpostID + @Atomic + private var _signpostID: Any? = nil + var signpostID: OSSignpostID? { + set { _signpostID = newValue } + get { _signpostID as? OSSignpostID } + } + + /// A time duration + @objc + public private(set) var duration: TimeInterval = 0 + + /// Accumulated interval statistics + public var statistics: IntervalStatistics { StatisticsStore.shared[id] } + + /// Text of this log message. + public override var text: String { + let statistics = self.statistics + let items: [(IntervalOptions, String, () -> Any)] = [ + (.duration, "duration", { stringFromTimeInterval(self.duration) }), + (.count, "count", { statistics.count }), + (.total, "total", { stringFromTimeInterval(statistics.total) }), + (.min, "min", { stringFromTimeInterval(statistics.min) }), + (.max, "max", { stringFromTimeInterval(statistics.max) }), + (.average, "average", { stringFromTimeInterval(statistics.average) }) + ] + let dict = dictionary(from: items, options: config.intervalConfig.options) + let text = [dict.json(), name].joinedCompact() + return text + } + + init(logger: DLog, name: String, staticName: StaticString?, category: String, config: LogConfig, scope: LogScope?, metadata: Metadata, file: String, funcName: String, line: UInt) { + self.logger = logger + self.name = name + self.id = "\(file):\(funcName):\(line)".hash + self.staticName = staticName + super.init(type: .interval, category: category, config: config, scope: scope, metadata: metadata, file: file, funcName: funcName, line: line, message: nil) + } + + /// Start a time interval. + /// + /// A time interval can be created and then used for logging running time statistics. + /// + /// let logger = DLog() + /// let interval = logger.interval("Sort") + /// interval.begin() + /// ... + /// interval.end() + /// + @objc + public func begin() { + guard !begun else { return } + begun.toggle() - let staticName: StaticString? - - // SignpostID - @Atomic - private var _signpostID: Any? = nil - var signpostID: OSSignpostID? { - set { _signpostID = newValue } - get { _signpostID as? OSSignpostID } - } + time = Date() + duration = 0 - /// A time duration - @objc - public private(set) var duration: TimeInterval = 0 + logger.begin(interval: self) + } + + /// Finish a time interval. + /// + /// A time interval can be created and then used for logging running time statistics. + /// + /// let logger = DLog() + /// let interval = logger.interval("Sort") + /// interval.begin() + /// ... + /// interval.end() + /// + @objc + public func end() { + guard begun else { return } + begun.toggle() - /// Accumulated interval statistics - public var statistics: IntervalStatistics { StatisticsStore.shared[id] } + duration = -time.timeIntervalSinceNow + time = Date() - /// Text of this log message. - public override var text: String { - let statistics = self.statistics - let items: [(IntervalOptions, String, () -> Any)] = [ - (.duration, "duration", { stringFromTimeInterval(self.duration) }), - (.count, "count", { statistics.count }), - (.total, "total", { stringFromTimeInterval(statistics.total) }), - (.min, "min", { stringFromTimeInterval(statistics.min) }), - (.max, "max", { stringFromTimeInterval(statistics.max) }), - (.average, "average", { stringFromTimeInterval(statistics.average) }) - ] - let dict = dictionary(from: items, options: config.intervalConfig.options) - let text = [dict.json(), name].joinedCompact() - return text + // Statistics + var record = self.statistics + record.count += 1 + record.total += duration + if record.min == 0 || record.min > duration { + record.min = duration } - - init(logger: DLog, name: String, staticName: StaticString?, category: String, config: LogConfig, scope: LogScope?, metadata: Metadata, file: String, funcName: String, line: UInt) { - self.logger = logger - self.name = name - self.id = "\(file):\(funcName):\(line)".hash - self.staticName = staticName - super.init(type: .interval, category: category, config: config, scope: scope, metadata: metadata, file: file, funcName: funcName, line: line, message: nil) - } - - /// Start a time interval. - /// - /// A time interval can be created and then used for logging running time statistics. - /// - /// let logger = DLog() - /// let interval = logger.interval("Sort") - /// interval.begin() - /// ... - /// interval.end() - /// - @objc - public func begin() { - guard !begun else { return } - begun.toggle() - - time = Date() - duration = 0 - - logger.begin(interval: self) - } - - /// Finish a time interval. - /// - /// A time interval can be created and then used for logging running time statistics. - /// - /// let logger = DLog() - /// let interval = logger.interval("Sort") - /// interval.begin() - /// ... - /// interval.end() - /// - @objc - public func end() { - guard begun else { return } - begun.toggle() - - duration = -time.timeIntervalSinceNow - time = Date() - - // Statistics - var record = self.statistics - record.count += 1 - record.total += duration - if record.min == 0 || record.min > duration { - record.min = duration - } - if record.max == 0 || record.max < duration { - record.max = duration - } - record.average = record.total / Double(record.count) - - StatisticsStore.shared[id] = record - - logger.end(interval: self) - } + if record.max == 0 || record.max < duration { + record.max = duration + } + record.average = record.total / Double(record.count) + + StatisticsStore.shared[id] = record + + logger.end(interval: self) + } } diff --git a/Sources/DLog/LogItem.swift b/Sources/DLog/LogItem.swift index 4f7fb0d..fdf40f1 100644 --- a/Sources/DLog/LogItem.swift +++ b/Sources/DLog/LogItem.swift @@ -32,32 +32,32 @@ import Foundation /// @objc public enum LogType : Int { - /// The default log level to capture non critical information. - case log - - /// The informational log level to capture information messages and helpful data. - case info - - /// The trace log level to capture the current function name to help in debugging problems during the development. - case trace - - /// The debug log level to capture information that may be useful during development or while troubleshooting a specific problem. - case debug - - /// The warning log level to capture information about things that might result in an error. - case warning - - /// The error log level to report errors. - case error - - /// The assert log level for sanity checks. - case assert - - /// The fault log level to capture system-level or multi-process information when reporting system errors. - case fault - - /// The interval log level. - case interval + /// The default log level to capture non critical information. + case log + + /// The informational log level to capture information messages and helpful data. + case info + + /// The trace log level to capture the current function name to help in debugging problems during the development. + case trace + + /// The debug log level to capture information that may be useful during development or while troubleshooting a specific problem. + case debug + + /// The warning log level to capture information about things that might result in an error. + case warning + + /// The error log level to report errors. + case error + + /// The assert log level for sanity checks. + case assert + + /// The fault log level to capture system-level or multi-process information when reporting system errors. + case fault + + /// The interval log level. + case interval } /// A base log message class that the logger adds to the logs. @@ -66,48 +66,48 @@ public enum LogType : Int { /// @objcMembers public class LogItem: NSObject { - /// The timestamp of this log message. - public internal(set) var time = Date() - - /// The category of this log message. - public let category: String - - /// The scope of this log message. - public let scope: LogScope? - - /// The log level of this log message. - public let type: LogType - - /// The file name this log message originates from. - public let fileName: String - - /// The function name this log message originates from. - public let funcName: String - - /// The line number of code this log message originates from. - public let line: UInt - - private var message: (() -> LogMessage)? - - /// Text of this log message. - public var text: String { - return message?().text ?? "" - } - - let config: LogConfig - - /// Metadata of log message - public let metadata: Metadata - - init(type: LogType, category: String, config: LogConfig, scope: LogScope?, metadata: Metadata, file: String, funcName: String, line: UInt, message: (() -> LogMessage)?) { - self.type = type - self.category = category - self.config = config - self.scope = scope - self.metadata = metadata - self.fileName = (file as NSString).lastPathComponent - self.funcName = funcName - self.line = line - self.message = message - } + /// The timestamp of this log message. + public internal(set) var time = Date() + + /// The category of this log message. + public let category: String + + /// The scope of this log message. + public let scope: LogScope? + + /// The log level of this log message. + public let type: LogType + + /// The file name this log message originates from. + public let fileName: String + + /// The function name this log message originates from. + public let funcName: String + + /// The line number of code this log message originates from. + public let line: UInt + + private var message: (() -> LogMessage)? + + /// Text of this log message. + public var text: String { + return message?().text ?? "" + } + + let config: LogConfig + + /// Metadata of log message + public let metadata: Metadata + + init(type: LogType, category: String, config: LogConfig, scope: LogScope?, metadata: Metadata, file: String, funcName: String, line: UInt, message: (() -> LogMessage)?) { + self.type = type + self.category = category + self.config = config + self.scope = scope + self.metadata = metadata + self.fileName = (file as NSString).lastPathComponent + self.funcName = funcName + self.line = line + self.message = message + } } diff --git a/Sources/DLog/LogMessage.swift b/Sources/DLog/LogMessage.swift index 920df24..fa91283 100644 --- a/Sources/DLog/LogMessage.swift +++ b/Sources/DLog/LogMessage.swift @@ -32,59 +32,59 @@ import Foundation /// a value of generic type in the string interpolations passed to the logger. /// public class LogStringInterpolation: StringInterpolationProtocol { - fileprivate var output = "" - - /// Creates an empty instance ready to be filled with string literal content. - public required init(literalCapacity: Int, interpolationCount: Int) { - output.reserveCapacity(literalCapacity * 2) - } - - /// Appends a literal segment to the interpolation. - public func appendLiteral(_ literal: String) { - output.append(literal) - } - - /// Defines interpolation for expressions of Any type. - public func appendInterpolation(_ value: @autoclosure @escaping () -> Any, privacy: LogPrivacy = .public) { - let text = String(describing: value()) - let masked = privacy.mask(text) - output.append(masked) - } - - /// Defines interpolation for expressions of date type. - public func appendInterpolation(_ value: @autoclosure @escaping () -> Date, format: LogDateFormatting, privacy: LogPrivacy = .public) { - let text = format.string(from: value()) - let masked = privacy.mask(text) - output.append(masked) - } - - /// Defines interpolation for expressions of integer values. - public func appendInterpolation(_ value: @autoclosure @escaping () -> T, format: LogIntFormatting, privacy: LogPrivacy = .public) { - let text = format.string(from: value()) - let masked = privacy.mask(text) - output.append(masked) - } - - /// Defines interpolation for expressions of floating-point values. - public func appendInterpolation(_ value: @autoclosure @escaping () -> T, format: LogFloatFormatting, privacy: LogPrivacy = .public) { - let text = format.string(from: value()) - let masked = privacy.mask(text) - output.append(masked) - } - - /// Defines interpolation for expressions of boolean values. - public func appendInterpolation(_ value: @autoclosure @escaping () -> Bool, format: LogBoolFormatting, privacy: LogPrivacy = .public) { - let text = format.string(from: value()) - let masked = privacy.mask(text) - output.append(masked) - } - - /// Defines interpolation for expressions of Data. - public func appendInterpolation(_ value: @autoclosure @escaping () -> Data, format: LogDataFormatting, privacy: LogPrivacy = .public) { - let text = format.string(from: value()) - let masked = privacy.mask(text) - output.append(masked) - } + fileprivate var output = "" + + /// Creates an empty instance ready to be filled with string literal content. + public required init(literalCapacity: Int, interpolationCount: Int) { + output.reserveCapacity(literalCapacity * 2) + } + + /// Appends a literal segment to the interpolation. + public func appendLiteral(_ literal: String) { + output.append(literal) + } + + /// Defines interpolation for expressions of Any type. + public func appendInterpolation(_ value: @autoclosure @escaping () -> Any, privacy: LogPrivacy = .public) { + let text = String(describing: value()) + let masked = privacy.mask(text) + output.append(masked) + } + + /// Defines interpolation for expressions of date type. + public func appendInterpolation(_ value: @autoclosure @escaping () -> Date, format: LogDateFormatting, privacy: LogPrivacy = .public) { + let text = format.string(from: value()) + let masked = privacy.mask(text) + output.append(masked) + } + + /// Defines interpolation for expressions of integer values. + public func appendInterpolation(_ value: @autoclosure @escaping () -> T, format: LogIntFormatting, privacy: LogPrivacy = .public) { + let text = format.string(from: value()) + let masked = privacy.mask(text) + output.append(masked) + } + + /// Defines interpolation for expressions of floating-point values. + public func appendInterpolation(_ value: @autoclosure @escaping () -> T, format: LogFloatFormatting, privacy: LogPrivacy = .public) { + let text = format.string(from: value()) + let masked = privacy.mask(text) + output.append(masked) + } + + /// Defines interpolation for expressions of boolean values. + public func appendInterpolation(_ value: @autoclosure @escaping () -> Bool, format: LogBoolFormatting, privacy: LogPrivacy = .public) { + let text = format.string(from: value()) + let masked = privacy.mask(text) + output.append(masked) + } + + /// Defines interpolation for expressions of Data. + public func appendInterpolation(_ value: @autoclosure @escaping () -> Data, format: LogDataFormatting, privacy: LogPrivacy = .public) { + let text = format.string(from: value()) + let masked = privacy.mask(text) + output.append(masked) + } } /// An object that represents a log message. @@ -96,47 +96,47 @@ public class LogStringInterpolation: StringInterpolationProtocol { /// when you pass a string interpolation to the logger. /// public class LogMessage: NSObject, ExpressibleByStringLiteral, - ExpressibleByIntegerLiteral, - ExpressibleByFloatLiteral, - ExpressibleByBooleanLiteral, - ExpressibleByArrayLiteral, - ExpressibleByDictionaryLiteral, - ExpressibleByStringInterpolation { - let text: String - - /// Creates an instance initialized to the given string value. - @objc - public required init(stringLiteral value: String) { - text = value - } - - /// Creates an instance initialized to the given integer value. - public required init(integerLiteral value: Int) { - text = "\(value)" - } - - /// Creates an instance initialized to the given float value. - public required init(floatLiteral value: Float) { - text = "\(value)" - } - - /// Creates an instance initialized to the given bool value. - public required init(booleanLiteral value: Bool) { - text = "\(value)" - } - - /// Creates an instance initialized to the given array. - public required init(arrayLiteral elements: Any...) { - text = "\(elements)" - } - - /// Creates an instance initialized to the given dictionary. - public required init(dictionaryLiteral elements: (Any, Any)...) { - text = "\(elements)" - } - - /// Creates an instance of a log message from a string interpolation. - public required init(stringInterpolation: LogStringInterpolation) { - text = stringInterpolation.output - } + ExpressibleByIntegerLiteral, + ExpressibleByFloatLiteral, + ExpressibleByBooleanLiteral, + ExpressibleByArrayLiteral, + ExpressibleByDictionaryLiteral, + ExpressibleByStringInterpolation { + let text: String + + /// Creates an instance initialized to the given string value. + @objc + public required init(stringLiteral value: String) { + text = value + } + + /// Creates an instance initialized to the given integer value. + public required init(integerLiteral value: Int) { + text = "\(value)" + } + + /// Creates an instance initialized to the given float value. + public required init(floatLiteral value: Float) { + text = "\(value)" + } + + /// Creates an instance initialized to the given bool value. + public required init(booleanLiteral value: Bool) { + text = "\(value)" + } + + /// Creates an instance initialized to the given array. + public required init(arrayLiteral elements: Any...) { + text = "\(elements)" + } + + /// Creates an instance initialized to the given dictionary. + public required init(dictionaryLiteral elements: (Any, Any)...) { + text = "\(elements)" + } + + /// Creates an instance of a log message from a string interpolation. + public required init(stringInterpolation: LogStringInterpolation) { + text = stringInterpolation.output + } } diff --git a/Sources/DLog/LogMetadata.swift b/Sources/DLog/LogMetadata.swift index 5ac9fa7..4a9f3c9 100644 --- a/Sources/DLog/LogMetadata.swift +++ b/Sources/DLog/LogMetadata.swift @@ -27,46 +27,46 @@ import Foundation public typealias Metadata = [String : Any] extension Metadata { - func json(parenthesis: Bool = false, pretty: Bool = false) -> String { - let options: JSONSerialization.WritingOptions = pretty ? [.sortedKeys, .prettyPrinted] : [.sortedKeys] - guard self.isEmpty == false, - let data = try? JSONSerialization.data(withJSONObject: self, options: options), - let json = String(data: data, encoding: .utf8) else { - return "" - } - var result = json - if parenthesis { - let start = json.index(after: json.startIndex) - let end = json.index(before: json.endIndex) - result = "(\(json[start.. String { + let options: JSONSerialization.WritingOptions = pretty ? [.sortedKeys, .prettyPrinted] : [.sortedKeys] + guard self.isEmpty == false, + let data = try? JSONSerialization.data(withJSONObject: self, options: options), + let json = String(data: data, encoding: .utf8) else { + return "" } + var result = json + if parenthesis { + let start = json.index(after: json.startIndex) + let end = json.index(before: json.endIndex) + result = "(\(json[start.. Any? { + get { + synchronized(self) { data[name] } } - - /// Gets and sets a value by a string key to metadata. - public subscript(name: String) -> Any? { - get { - synchronized(self) { data[name] } - } - set { - synchronized(self) { data[name] = newValue } - } + set { + synchronized(self) { data[name] = newValue } } - - /// Clears metadata - public func clear() { - synchronized(self) { - data.removeAll() - } + } + + /// Clears metadata + public func clear() { + synchronized(self) { + data.removeAll() } + } } diff --git a/Sources/DLog/LogOutput.swift b/Sources/DLog/LogOutput.swift index a10aac9..ae3fe71 100644 --- a/Sources/DLog/LogOutput.swift +++ b/Sources/DLog/LogOutput.swift @@ -28,110 +28,110 @@ import Foundation /// A base output class. @objcMembers public class LogOutput : NSObject { - /// Creates `Text` output with plain style. - public static var textPlain: Text { Text(style: .plain) } - - /// Creates `Text` output with emoji style. - public static var textEmoji: Text { Text(style: .emoji) } - - /// Creates `Text` output with colored style (ANSI escape codes). - public static var textColored: Text { Text(style: .colored) } - - /// Creates `Standard` output for `stdout` stream. - @objc(stdOut) - public static var stdout: Standard { Standard() } - - /// Creates `Standard` output for `stderr` stream. - @objc(stdErr) - public static var stderr: Standard { Standard(stream: Darwin.stderr) } - - /// Creates `OSLog` output with default subsystem name: `com.dlog.logger`. - public static var oslog: OSLog { OSLog() } - - /// Creates `OSLog` output with a subsystem name. - public static func oslog(_ subsystem: String) -> OSLog { OSLog(subsystem: subsystem) } - - /// Creates `Filter` output for log items. - public static func filter(item: @escaping (LogItem) -> Bool) -> Filter { - Filter(isItem: item, isScope: nil) - } - - /// Creates `Filter` output for log scopes. - public static func filter(scope: @escaping (LogScope) -> Bool) -> Filter { - Filter(isItem: nil, isScope: scope) - } - - /// Creates `File` output with a file path to write. - public static func file(_ path: String, append: Bool = false) -> File { File(path: path, append: append) } - + /// Creates `Text` output with plain style. + public static var textPlain: Text { Text(style: .plain) } + + /// Creates `Text` output with emoji style. + public static var textEmoji: Text { Text(style: .emoji) } + + /// Creates `Text` output with colored style (ANSI escape codes). + public static var textColored: Text { Text(style: .colored) } + + /// Creates `Standard` output for `stdout` stream. + @objc(stdOut) + public static var stdout: Standard { Standard() } + + /// Creates `Standard` output for `stderr` stream. + @objc(stdErr) + public static var stderr: Standard { Standard(stream: Darwin.stderr) } + + /// Creates `OSLog` output with default subsystem name: `com.dlog.logger`. + public static var oslog: OSLog { OSLog() } + + /// Creates `OSLog` output with a subsystem name. + public static func oslog(_ subsystem: String) -> OSLog { OSLog(subsystem: subsystem) } + + /// Creates `Filter` output for log items. + public static func filter(item: @escaping (LogItem) -> Bool) -> Filter { + Filter(isItem: item, isScope: nil) + } + + /// Creates `Filter` output for log scopes. + public static func filter(scope: @escaping (LogScope) -> Bool) -> Filter { + Filter(isItem: nil, isScope: scope) + } + + /// Creates `File` output with a file path to write. + public static func file(_ path: String, append: Bool = false) -> File { File(path: path, append: append) } + #if !os(watchOS) - /// Creates `Net` output for default service name: `DLog`. - public static var net: Net { Net() } - - /// Creates `Net` output for a service name. - public static func net(_ name: String) -> Net { Net(name: name) } + /// Creates `Net` output for default service name: `DLog`. + public static var net: Net { Net() } + + /// Creates `Net` output for a service name. + public static func net(_ name: String) -> Net { Net(name: name) } #endif - - /// A source output. - public var source: LogOutput! - - init(source: LogOutput?) { - self.source = source - } - - @discardableResult - func log(item: LogItem) -> String? { - return source != nil - ? source.log(item: item) - : nil - } - - @discardableResult - func scopeEnter(scope: LogScope) -> String? { - return source != nil - ? source.scopeEnter(scope: scope) - : nil - } - - @discardableResult - func scopeLeave(scope: LogScope) -> String? { - return source != nil - ? source.scopeLeave(scope: scope) - : nil - } - - func intervalBegin(interval: LogInterval) { - if source != nil { - source.intervalBegin(interval: interval) - } - } - - @discardableResult - func intervalEnd(interval: LogInterval) -> String? { - return source != nil - ? source.intervalEnd(interval: interval) - : nil - } + + /// A source output. + public var source: LogOutput! + + init(source: LogOutput?) { + self.source = source + } + + @discardableResult + func log(item: LogItem) -> String? { + return source != nil + ? source.log(item: item) + : nil + } + + @discardableResult + func scopeEnter(scope: LogScope) -> String? { + return source != nil + ? source.scopeEnter(scope: scope) + : nil + } + + @discardableResult + func scopeLeave(scope: LogScope) -> String? { + return source != nil + ? source.scopeLeave(scope: scope) + : nil + } + + func intervalBegin(interval: LogInterval) { + if source != nil { + source.intervalBegin(interval: interval) + } + } + + @discardableResult + func intervalEnd(interval: LogInterval) -> String? { + return source != nil + ? source.intervalEnd(interval: interval) + : nil + } } // Forward pipe precedencegroup ForwardPipe { - associativity: left + associativity: left } /// Pipeline operator which defines a combined output from two outputs /// where the first one is a source and second is a target infix operator => : ForwardPipe extension LogOutput { - - /// Forward pipe operator - /// - /// The operator allows to create a list of linked outputs. - /// - /// let logger = DLog(.textEmoji => .stdout => .file("dlog.txt")) - /// - public static func => (left: LogOutput, right: LogOutput) -> LogOutput { - right.source = left - return right - } + + /// Forward pipe operator + /// + /// The operator allows to create a list of linked outputs. + /// + /// let logger = DLog(.textEmoji => .stdout => .file("dlog.txt")) + /// + public static func => (left: LogOutput, right: LogOutput) -> LogOutput { + right.source = left + return right + } } diff --git a/Sources/DLog/LogPrivacy.swift b/Sources/DLog/LogPrivacy.swift index 60f613a..af83573 100644 --- a/Sources/DLog/LogPrivacy.swift +++ b/Sources/DLog/LogPrivacy.swift @@ -28,225 +28,225 @@ import Foundation /// The privacy options that determine when to redact or display values in log messages. public enum LogPrivacy { + + /// Mask options for `private` privacy level. + public enum Mask { + /// Hash a value in the logs. + case hash - /// Mask options for `private` privacy level. - public enum Mask { - /// Hash a value in the logs. - case hash - - /// Randomise a value in the logs. - case random - - /// Redact a value in the logs. - case redact - - /// Shuffle a value in the logs. - case shuffle - - /// Replace a value with a custom string in the logs. - /// - Parameters: - /// - value: A custom string. - case custom(value: String) - - /// Reduce a value to a string with specified length in the logs. - /// - Parameters: - /// - length: Length of an output string. - case reduce(length: Int) - - /// Show parts of a value in the logs. - /// - Parameters: - /// - first: Count of first visible characters. - /// - last: Count of last visible characters. - case partial(first: Int, last: Int) - } + /// Randomise a value in the logs. + case random + + /// Redact a value in the logs. + case redact - /// Sets the privacy level of a log message to public. - /// - /// When the privacy level is public, the value will be displayed - /// normally without any redaction in the logs. - case `public` + /// Shuffle a value in the logs. + case shuffle + + /// Replace a value with a custom string in the logs. + /// - Parameters: + /// - value: A custom string. + case custom(value: String) - /// Sets the privacy level of a log message to private and - /// applies a `mask` to redacted it. - /// - /// When the privacy level is private, the value will be redacted in the logs. - /// + /// Reduce a value to a string with specified length in the logs. /// - Parameters: - /// - mask: Mask to use with the privacy option. - /// - auto: If `true` the mask is not applied while debugging. If `false` the mask is applied both in Debug and Release. Defaults to `true`. - case `private`(mask: Mask, auto: Bool = true) + /// - length: Length of an output string. + case reduce(length: Int) - /// Sets the privacy level of a log message to private. - /// - /// When the privacy level is private, the value will be redacted in the logs. - /// - public static var `private`: Self { - .private(mask: .custom(value: "")) + /// Show parts of a value in the logs. + /// - Parameters: + /// - first: Count of first visible characters. + /// - last: Count of last visible characters. + case partial(first: Int, last: Int) + } + + /// Sets the privacy level of a log message to public. + /// + /// When the privacy level is public, the value will be displayed + /// normally without any redaction in the logs. + case `public` + + /// Sets the privacy level of a log message to private and + /// applies a `mask` to redacted it. + /// + /// When the privacy level is private, the value will be redacted in the logs. + /// + /// - Parameters: + /// - mask: Mask to use with the privacy option. + /// - auto: If `true` the mask is not applied while debugging. If `false` the mask is applied both in Debug and Release. Defaults to `true`. + case `private`(mask: Mask, auto: Bool = true) + + /// Sets the privacy level of a log message to private. + /// + /// When the privacy level is private, the value will be redacted in the logs. + /// + public static var `private`: Self { + .private(mask: .custom(value: "")) + } + + private static let isDebugger: Bool = { + var info = kinfo_proc() + var size = MemoryLayout.size(ofValue: info) + var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] + return sysctl(&mib, UInt32(mib.count), &info, &size, nil, 0) == 0 && + (info.kp_proc.p_flag & P_TRACED) != 0 + }() + + private static let isXCTest: Bool = { NSClassFromString("XCTest") != nil }() + + private static let letters: [Character] = { + let lower = (Unicode.Scalar("a").value...Unicode.Scalar("z").value) + let upper = (Unicode.Scalar("A").value...Unicode.Scalar("Z").value) + return [lower, upper].joined() + .compactMap(UnicodeScalar.init) + .map(Character.init) + }() + + private static let digits: [Character] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] + + private static let symbols: [Character] = ["!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "_", "=", "+", + "[", "{", "]", "}", "\\", "|", + ";", ":", "'", "\"", + ",", "<", ".", ">", "/", "?"] + + private static func redact(_ text: String) -> String { + let count = text.count + guard count > 0 else { + return text } - private static let isDebugger: Bool = { - var info = kinfo_proc() - var size = MemoryLayout.size(ofValue: info) - var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] - return sysctl(&mib, UInt32(mib.count), &info, &size, nil, 0) == 0 && - (info.kp_proc.p_flag & P_TRACED) != 0 - }() + var array = Array(text) + for i in 0...count-1 { + let c = array[i] + if c.isLetter { + array[i] = "X" + } + else if c.isNumber { + array[i] = "0" + } + } + return String(array) + } + + private static func reduce(_ text: String, length: Int) -> String { + let l = length > 0 ? length : 0 + guard l > 0 else { + return "..." + } - private static let isXCTest: Bool = { NSClassFromString("XCTest") != nil }() + let count = text.count + guard count > 0, l < count else { + return text + } - private static let letters: [Character] = { - let lower = (Unicode.Scalar("a").value...Unicode.Scalar("z").value) - let upper = (Unicode.Scalar("A").value...Unicode.Scalar("Z").value) - return [lower, upper].joined() - .compactMap(UnicodeScalar.init) - .map(Character.init) - }() + let m = Int(l / 2) + let s = (l % 2 == 0) ? m : m + let e = (l % 2 == 0) ? m : m + 1 + + let start = text.index(text.startIndex, offsetBy: s) + let end = text.index(text.endIndex, offsetBy: -(e + 1)) + return text.replacingCharacters(in: start...end, with: "...") + } + + private static func partial(_ text: String, first: Int, last: Int) -> String { + let count = text.count + guard count > 0 else { + return text + } - private static let digits: [Character] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] + let f = first > 0 ? first : 0 + let l = last > 0 ? last : 0 - private static let symbols: [Character] = ["!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "_", "=", "+", - "[", "{", "]", "}", "\\", "|", - ";", ":", "'", "\"", - ",", "<", ".", ">", "/", "?"] + guard count > f + l else { + return text + } - private static func redact(_ text: String) -> String { - let count = text.count - guard count > 0 else { - return text + let start = text.index(text.startIndex, offsetBy: f) + let end = text.index(text.endIndex, offsetBy: -(l + 1)) + let replacement = String(repeating: "*", count: count - (f + l)) + return text.replacingCharacters(in: start...end, with: replacement) + } + + private static func shuffle(_ text: String) -> String { + text.components(separatedBy: .whitespacesAndNewlines) + .map { + let count = $0.count + guard count > 1 else { + return $0 } - var array = Array(text) - for i in 0...count-1 { - let c = array[i] - if c.isLetter { - array[i] = "X" - } - else if c.isNumber { - array[i] = "0" - } + var array = Array($0) + for i in 0...count-2 { + let j = Int.random(in: i+1...count-1) + let item = array[i] + array[i] = array[j] + array[j] = item } return String(array) + } + .joined(separator: " ") + } + + private static func random(_ text: String) -> String { + let count = text.count + guard count > 0 else { + return text } - private static func reduce(_ text: String, length: Int) -> String { - let l = length > 0 ? length : 0 - guard l > 0 else { - return "..." - } - - let count = text.count - guard count > 0, l < count else { - return text - } - - let m = Int(l / 2) - let s = (l % 2 == 0) ? m : m - let e = (l % 2 == 0) ? m : m + 1 - - let start = text.index(text.startIndex, offsetBy: s) - let end = text.index(text.endIndex, offsetBy: -(e + 1)) - return text.replacingCharacters(in: start...end, with: "...") + var array = Array(text) + for i in 0...count-1 { + let char = array[i] + if char.isLetter { + array[i] = letters.randomElement()! + } + else if char.isNumber { + array[i] = digits.randomElement()! + } + else if char.isMathSymbol || char.isPunctuation { + array[i] = symbols.randomElement()! + } } - - private static func partial(_ text: String, first: Int, last: Int) -> String { - let count = text.count - guard count > 0 else { - return text - } - - let f = first > 0 ? first : 0 - let l = last > 0 ? last : 0 + return String(array) + } + + + func mask(_ text: String) -> String { + switch self { - guard count > f + l else { - return text - } + case .public: + return text - let start = text.index(text.startIndex, offsetBy: f) - let end = text.index(text.endIndex, offsetBy: -(l + 1)) - let replacement = String(repeating: "*", count: count - (f + l)) - return text.replacingCharacters(in: start...end, with: replacement) - } - - private static func shuffle(_ text: String) -> String { - text.components(separatedBy: .whitespacesAndNewlines) - .map { - let count = $0.count - guard count > 1 else { - return $0 - } - - var array = Array($0) - for i in 0...count-2 { - let j = Int.random(in: i+1...count-1) - let item = array[i] - array[i] = array[j] - array[j] = item - } - return String(array) - } - .joined(separator: " ") - } - - private static func random(_ text: String) -> String { - let count = text.count - guard count > 0 else { - return text + case let .private(mask, auto): + // Skip for debugging and testing + guard !auto || !Self.isDebugger || Self.isXCTest else { + return text } - var array = Array(text) - for i in 0...count-1 { - let char = array[i] - if char.isLetter { - array[i] = letters.randomElement()! - } - else if char.isNumber { - array[i] = digits.randomElement()! - } - else if char.isMathSymbol || char.isPunctuation { - array[i] = symbols.randomElement()! - } - } - return String(array) - } - - - func mask(_ text: String) -> String { - switch self { + switch mask { + + case .hash: + return String(format: "%02X", text.hashValue) + + case .redact: + return Self.redact(text) + + case .shuffle: + return Self.shuffle(text) + + case .random: + return Self.random(text) - case .public: - return text + case .reduce(let length): + return Self.reduce(text, length: length) - case let .private(mask, auto): - // Skip for debugging and testing - guard !auto || !Self.isDebugger || Self.isXCTest else { - return text - } + case let .partial(first, last): + return Self.partial(text, first: first, last: last) - switch mask { - - case .hash: - return String(format: "%02X", text.hashValue) - - case .redact: - return Self.redact(text) - - case .shuffle: - return Self.shuffle(text) - - case .random: - return Self.random(text) - - case .reduce(let length): - return Self.reduce(text, length: length) - - case let .partial(first, last): - return Self.partial(text, first: first, last: last) - - case .custom(let value): - return value - } + case .custom(let value): + return value } } + } } diff --git a/Sources/DLog/LogProtocol.swift b/Sources/DLog/LogProtocol.swift index 3aab8f9..3ff6750 100644 --- a/Sources/DLog/LogProtocol.swift +++ b/Sources/DLog/LogProtocol.swift @@ -29,257 +29,257 @@ import Foundation /// @objcMembers public class LogProtocol: NSObject { - var logger: DLog! - let category: String - let config: LogConfig - var _scope: LogScope? - - /// Contextual metadata - public let metadata: LogMetadata - - init(logger: DLog?, category: String, config: LogConfig, metadata: Metadata) { - self.logger = logger - self.category = category - self.config = config - self.metadata = LogMetadata(data: metadata) + var logger: DLog! + let category: String + let config: LogConfig + var _scope: LogScope? + + /// Contextual metadata + public let metadata: LogMetadata + + init(logger: DLog?, category: String, config: LogConfig, metadata: Metadata) { + self.logger = logger + self.category = category + self.config = config + self.metadata = LogMetadata(data: metadata) + } + + /// Logs a message that is essential to troubleshoot problems later. + /// + /// This method logs the message using the default log level. + /// + /// let logger = DLog() + /// logger.log("message") + /// + /// - Parameters: + /// - message: The message to be logged that can be used with any string interpolation literal. + /// - file: The file this log message originates from (defaults to `#file`). + /// - function: The function this log message originates from (defaults to `#function`). + /// - line: The line this log message originates (defaults to `#line`). + /// + /// - Returns: Returns an optional string value indicating whether a log message is generated and processed. + /// + @discardableResult + public func log(_ message: @escaping @autoclosure () -> LogMessage, file: String = #file, function: String = #function, line: UInt = #line) -> String? { + return logger.log(message: message, type: .log, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, function: function, line: line) + } + + /// Logs trace information to help debug problems during the development of your code. + /// + /// Use it during development to record information that might aid you in debugging problems later. + /// + /// let logger = DLog() + /// logger.trace("message") + /// + /// - Parameters: + /// - message: The message to be logged that can be used with any string interpolation literal. + /// - file: The file this log message originates from (defaults to `#file`). + /// - function: The function this log message originates from (defaults to `#function`). + /// - line: The line this log message originates (defaults to `#line`). + /// + /// - Returns: Returns an optional string value indicating whether a log message is generated and processed. + /// + @discardableResult + public func trace(_ message: @escaping @autoclosure () -> LogMessage? = nil, + file: String = #file, function: String = #function, line: UInt = #line, + addresses: [NSNumber] = Thread.callStackReturnAddresses) -> String? { + let msg: () -> LogMessage = { + let info = traceInfo(text: message()?.text, + function: function, + addresses: addresses.dropFirst(), + traceConfig: self.config.traceConfig) + return LogMessage(stringLiteral: info) } - - /// Logs a message that is essential to troubleshoot problems later. - /// - /// This method logs the message using the default log level. - /// - /// let logger = DLog() - /// logger.log("message") - /// - /// - Parameters: - /// - message: The message to be logged that can be used with any string interpolation literal. - /// - file: The file this log message originates from (defaults to `#file`). - /// - function: The function this log message originates from (defaults to `#function`). - /// - line: The line this log message originates (defaults to `#line`). - /// - /// - Returns: Returns an optional string value indicating whether a log message is generated and processed. - /// - @discardableResult - public func log(_ message: @escaping @autoclosure () -> LogMessage, file: String = #file, function: String = #function, line: UInt = #line) -> String? { - return logger.log(message: message, type: .log, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, function: function, line: line) + return logger.log(message: msg, type: .trace, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, function: function, line: line) + } + + /// Logs a message to help debug problems during the development of your code. + /// + /// Use this method during development to record information that might aid you in debugging problems later. + /// + /// let logger = DLog() + /// logger.debug("message") + /// + /// - Parameters: + /// - message: The message to be logged that can be used with any string interpolation literal. + /// - file: The file this log message originates from (defaults to `#file`). + /// - function: The function this log message originates from (defaults to `#function`). + /// - line: The line this log message originates (defaults to `#line`). + /// + /// - Returns: Returns an optional string value indicating whether a log message is generated and processed. + /// + @discardableResult + public func debug(_ message: @escaping @autoclosure () -> LogMessage, file: String = #file, function: String = #function, line: UInt = #line) -> String? { + return logger.log(message: message, type: .debug, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, function: function, line: line) + } + + /// Logs a message that is helpful, but not essential, to diagnose issues with your code. + /// + /// Use this method to capture information messages and helpful data. + /// + /// let logger = DLog() + /// logger.info("message") + /// + /// - Parameters: + /// - message: The message to be logged that can be used with any string interpolation literal. + /// - file: The file this log message originates from (defaults to `#file`). + /// - function: The function this log message originates from (defaults to `#function`). + /// - line: The line this log message originates (defaults to `#line`). + /// + /// - Returns: Returns an optional string value indicating whether a log message is generated and processed. + /// + @discardableResult + public func info(_ message: @escaping @autoclosure () -> LogMessage, file: String = #file, function: String = #function, line: UInt = #line) -> String? { + return logger.log(message: message, type: .info, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, function: function, line: line) + } + + /// Logs a warning that occurred during the execution of your code. + /// + /// Use this method to capture information about things that might result in an error. + /// + /// let logger = DLog() + /// logger.warning("message") + /// + /// - Parameters: + /// - message: The message to be logged that can be used with any string interpolation literal. + /// - file: The file this log message originates from (defaults to `#file`). + /// - function: The function this log message originates from (defaults to `#function`). + /// - line: The line this log message originates (defaults to `#line`). + /// + /// - Returns: Returns an optional string value indicating whether a log message is generated and processed. + /// + @discardableResult + public func warning(_ message: @escaping @autoclosure () -> LogMessage, file: String = #file, function: String = #function, line: UInt = #line) -> String? { + return logger.log(message: message, type: .warning, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, function: function, line: line) + } + + /// Logs an error that occurred during the execution of your code. + /// + /// Use this method to report errors. + /// + /// let logger = DLog() + /// logger.error("message") + /// + /// - Parameters: + /// - message: The message to be logged that can be used with any string interpolation literal. + /// - file: The file this log message originates from (defaults to `#file`). + /// - function: The function this log message originates from (defaults to `#function`). + /// - line: The line this log message originates (defaults to `#line`). + /// + /// - Returns: Returns an optional string value indicating whether a log message is generated and processed. + /// + @discardableResult + public func error(_ message: @escaping @autoclosure () -> LogMessage, file: String = #file, function: String = #function, line: UInt = #line) -> String? { + return logger.log(message: message, type: .error, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, function: function, line: line) + } + + /// Logs a traditional C-style assert notice with an optional message. + /// + /// Use this function for internal sanity checks. + /// + /// let logger = DLog() + /// logger.assert(condition, "message") + /// + /// - Parameters: + /// - condition: The condition to test. + /// - message: A string to print if `condition` is evaluated to `false`. The default is an empty string. + /// - file: The file this log message originates from (defaults to `#file`). + /// - function: The function this log message originates from (defaults to `#function`). + /// - line: The line this log message originates (defaults to `#line`). + /// + /// - Returns: Returns an optional string value indicating whether a log message is generated and processed. + /// + @discardableResult + public func assert(_ condition: @autoclosure () -> Bool, _ message: @escaping @autoclosure () -> LogMessage = "", file: String = #file, function: String = #function, line: UInt = #line) -> String? { + guard logger != .disabled && !condition() else { return nil } + return logger.log(message: message, type: .assert, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, function: function, line: line) + } + + /// Logs a bug or fault that occurred during the execution of your code. + /// + /// Use this method to capture critical errors that occurred during the execution of your code. + /// + /// let logger = DLog() + /// logger.fault("message") + /// + /// - Parameters: + /// - message: The message to be logged that can be used with any string interpolation literal. + /// - file: The file this log message originates from (defaults to `#file`). + /// - function: The function this log message originates from (defaults to `#function`). + /// - line: The line this log message originates (defaults to `#line`). + /// + /// - Returns: Returns an optional string value indicating whether a log message is generated and processed. + /// + @discardableResult + public func fault(_ message: @escaping @autoclosure () -> LogMessage, file: String = #file, function: String = #function, line: UInt = #line) -> String? { + return logger.log(message: message, type: .fault, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, function: function, line: line) + } + + /// Creates a scope object that can assign log messages to itself. + /// + /// Scope provides a mechanism for grouping log messages in your program. + /// + /// let logger = DLog() + /// logger.scope("Auth") { scope in + /// scope.log("message") + /// } + /// + /// - Parameters: + /// - name: The name of new scope object. + /// - file: The file this log message originates from (defaults to `#file`). + /// - function: The function this log message originates from (defaults to `#function`). + /// - line: The line this log message originates (defaults to `#line`). + /// - closure: A closure to be executed with the scope. The block takes a single `LogScope` parameter and has no return value. + /// + /// - Returns: An `LogScope` object for the new scope. + /// + @discardableResult + public func scope(_ name: String, metadata: Metadata? = nil, file: String = #file, function: String = #function, line: UInt = #line, closure: ((LogScope) -> Void)? = nil) -> LogScope { + let scope = LogScope(name: name, logger: logger, category: category, config: self.config, metadata: metadata ?? self.logger.metadata.data) + if let block = closure { + scope.enter() + block(scope) + scope.leave() } - - /// Logs trace information to help debug problems during the development of your code. - /// - /// Use it during development to record information that might aid you in debugging problems later. - /// - /// let logger = DLog() - /// logger.trace("message") - /// - /// - Parameters: - /// - message: The message to be logged that can be used with any string interpolation literal. - /// - file: The file this log message originates from (defaults to `#file`). - /// - function: The function this log message originates from (defaults to `#function`). - /// - line: The line this log message originates (defaults to `#line`). - /// - /// - Returns: Returns an optional string value indicating whether a log message is generated and processed. - /// - @discardableResult - public func trace(_ message: @escaping @autoclosure () -> LogMessage? = nil, - file: String = #file, function: String = #function, line: UInt = #line, - addresses: [NSNumber] = Thread.callStackReturnAddresses) -> String? { - let msg: () -> LogMessage = { - let info = traceInfo(text: message()?.text, - function: function, - addresses: addresses.dropFirst(), - traceConfig: self.config.traceConfig) - return LogMessage(stringLiteral: info) - } - return logger.log(message: msg, type: .trace, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, function: function, line: line) - } - - /// Logs a message to help debug problems during the development of your code. - /// - /// Use this method during development to record information that might aid you in debugging problems later. - /// - /// let logger = DLog() - /// logger.debug("message") - /// - /// - Parameters: - /// - message: The message to be logged that can be used with any string interpolation literal. - /// - file: The file this log message originates from (defaults to `#file`). - /// - function: The function this log message originates from (defaults to `#function`). - /// - line: The line this log message originates (defaults to `#line`). - /// - /// - Returns: Returns an optional string value indicating whether a log message is generated and processed. - /// - @discardableResult - public func debug(_ message: @escaping @autoclosure () -> LogMessage, file: String = #file, function: String = #function, line: UInt = #line) -> String? { - return logger.log(message: message, type: .debug, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, function: function, line: line) - } - - /// Logs a message that is helpful, but not essential, to diagnose issues with your code. - /// - /// Use this method to capture information messages and helpful data. - /// - /// let logger = DLog() - /// logger.info("message") - /// - /// - Parameters: - /// - message: The message to be logged that can be used with any string interpolation literal. - /// - file: The file this log message originates from (defaults to `#file`). - /// - function: The function this log message originates from (defaults to `#function`). - /// - line: The line this log message originates (defaults to `#line`). - /// - /// - Returns: Returns an optional string value indicating whether a log message is generated and processed. - /// - @discardableResult - public func info(_ message: @escaping @autoclosure () -> LogMessage, file: String = #file, function: String = #function, line: UInt = #line) -> String? { - return logger.log(message: message, type: .info, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, function: function, line: line) - } - - /// Logs a warning that occurred during the execution of your code. - /// - /// Use this method to capture information about things that might result in an error. - /// - /// let logger = DLog() - /// logger.warning("message") - /// - /// - Parameters: - /// - message: The message to be logged that can be used with any string interpolation literal. - /// - file: The file this log message originates from (defaults to `#file`). - /// - function: The function this log message originates from (defaults to `#function`). - /// - line: The line this log message originates (defaults to `#line`). - /// - /// - Returns: Returns an optional string value indicating whether a log message is generated and processed. - /// - @discardableResult - public func warning(_ message: @escaping @autoclosure () -> LogMessage, file: String = #file, function: String = #function, line: UInt = #line) -> String? { - return logger.log(message: message, type: .warning, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, function: function, line: line) - } - - /// Logs an error that occurred during the execution of your code. - /// - /// Use this method to report errors. - /// - /// let logger = DLog() - /// logger.error("message") - /// - /// - Parameters: - /// - message: The message to be logged that can be used with any string interpolation literal. - /// - file: The file this log message originates from (defaults to `#file`). - /// - function: The function this log message originates from (defaults to `#function`). - /// - line: The line this log message originates (defaults to `#line`). - /// - /// - Returns: Returns an optional string value indicating whether a log message is generated and processed. - /// - @discardableResult - public func error(_ message: @escaping @autoclosure () -> LogMessage, file: String = #file, function: String = #function, line: UInt = #line) -> String? { - return logger.log(message: message, type: .error, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, function: function, line: line) - } - - /// Logs a traditional C-style assert notice with an optional message. - /// - /// Use this function for internal sanity checks. - /// - /// let logger = DLog() - /// logger.assert(condition, "message") - /// - /// - Parameters: - /// - condition: The condition to test. - /// - message: A string to print if `condition` is evaluated to `false`. The default is an empty string. - /// - file: The file this log message originates from (defaults to `#file`). - /// - function: The function this log message originates from (defaults to `#function`). - /// - line: The line this log message originates (defaults to `#line`). - /// - /// - Returns: Returns an optional string value indicating whether a log message is generated and processed. - /// - @discardableResult - public func assert(_ condition: @autoclosure () -> Bool, _ message: @escaping @autoclosure () -> LogMessage = "", file: String = #file, function: String = #function, line: UInt = #line) -> String? { - guard logger != .disabled && !condition() else { return nil } - return logger.log(message: message, type: .assert, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, function: function, line: line) - } - - /// Logs a bug or fault that occurred during the execution of your code. - /// - /// Use this method to capture critical errors that occurred during the execution of your code. - /// - /// let logger = DLog() - /// logger.fault("message") - /// - /// - Parameters: - /// - message: The message to be logged that can be used with any string interpolation literal. - /// - file: The file this log message originates from (defaults to `#file`). - /// - function: The function this log message originates from (defaults to `#function`). - /// - line: The line this log message originates (defaults to `#line`). - /// - /// - Returns: Returns an optional string value indicating whether a log message is generated and processed. - /// - @discardableResult - public func fault(_ message: @escaping @autoclosure () -> LogMessage, file: String = #file, function: String = #function, line: UInt = #line) -> String? { - return logger.log(message: message, type: .fault, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, function: function, line: line) - } - - /// Creates a scope object that can assign log messages to itself. - /// - /// Scope provides a mechanism for grouping log messages in your program. - /// - /// let logger = DLog() - /// logger.scope("Auth") { scope in - /// scope.log("message") - /// } - /// - /// - Parameters: - /// - name: The name of new scope object. - /// - file: The file this log message originates from (defaults to `#file`). - /// - function: The function this log message originates from (defaults to `#function`). - /// - line: The line this log message originates (defaults to `#line`). - /// - closure: A closure to be executed with the scope. The block takes a single `LogScope` parameter and has no return value. - /// - /// - Returns: An `LogScope` object for the new scope. - /// - @discardableResult - public func scope(_ name: String, metadata: Metadata? = nil, file: String = #file, function: String = #function, line: UInt = #line, closure: ((LogScope) -> Void)? = nil) -> LogScope { - let scope = LogScope(name: name, logger: logger, category: category, config: self.config, metadata: metadata ?? self.logger.metadata.data) - if let block = closure { - scope.enter() - block(scope) - scope.leave() - } - return scope - } - - private func interval(name: String, staticName: StaticString?, file: String, function: String, line: UInt, closure: (() -> Void)?) -> LogInterval { - let interval = LogInterval(logger: logger, name: name, staticName: staticName, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, funcName: function, line: line) - if let block = closure { - interval.begin() - block() - interval.end() - } - return interval - } - - /// Creates an interval object that logs a detailed message with accumulated statistics. - /// - /// Logs a point of interest in your code as time intervals for debugging performances. - /// - /// let logger = DLog() - /// logger.interval("Sorting") { - /// ... - /// } - /// - /// - Parameters: - /// - name: The name of new interval object. - /// - file: The file this log message originates from (defaults to `#file`). - /// - function: The function this log message originates from (defaults to `#function`). - /// - line: The line this log message originates (defaults to `#line`). - /// - closure: A closure to be executed with the interval. - /// - /// - Returns: An `LogInterval` object for the new interval. - /// - @discardableResult - public func interval(_ name: StaticString, file: String = #file, function: String = #function, line: UInt = #line, closure: (() -> Void)? = nil) -> LogInterval { - return interval(name: "\(name)", staticName: name, file: file, function: function, line: line, closure: closure) - } - - /// Creates an interval object for Objective-C code. - @discardableResult - public func interval(name: String, file: String, function: String, line: UInt, closure: (() -> Void)?) -> LogInterval { - return interval(name: name, staticName: nil, file: file, function: function, line: line, closure: closure) + return scope + } + + private func interval(name: String, staticName: StaticString?, file: String, function: String, line: UInt, closure: (() -> Void)?) -> LogInterval { + let interval = LogInterval(logger: logger, name: name, staticName: staticName, category: category, config: config, scope: _scope, metadata: metadata.data, file: file, funcName: function, line: line) + if let block = closure { + interval.begin() + block() + interval.end() } + return interval + } + + /// Creates an interval object that logs a detailed message with accumulated statistics. + /// + /// Logs a point of interest in your code as time intervals for debugging performances. + /// + /// let logger = DLog() + /// logger.interval("Sorting") { + /// ... + /// } + /// + /// - Parameters: + /// - name: The name of new interval object. + /// - file: The file this log message originates from (defaults to `#file`). + /// - function: The function this log message originates from (defaults to `#function`). + /// - line: The line this log message originates (defaults to `#line`). + /// - closure: A closure to be executed with the interval. + /// + /// - Returns: An `LogInterval` object for the new interval. + /// + @discardableResult + public func interval(_ name: StaticString, file: String = #file, function: String = #function, line: UInt = #line, closure: (() -> Void)? = nil) -> LogInterval { + return interval(name: "\(name)", staticName: name, file: file, function: function, line: line, closure: closure) + } + + /// Creates an interval object for Objective-C code. + @discardableResult + public func interval(name: String, file: String, function: String, line: UInt, closure: (() -> Void)?) -> LogInterval { + return interval(name: name, staticName: nil, file: file, function: function, line: line, closure: closure) + } } diff --git a/Sources/DLog/LogScope.swift b/Sources/DLog/LogScope.swift index 45e58e8..a2b823a 100644 --- a/Sources/DLog/LogScope.swift +++ b/Sources/DLog/LogScope.swift @@ -27,38 +27,38 @@ import Foundation import os.log class ScopeStack { - static let shared = ScopeStack() - - private var scopes = [LogScope]() - - func exists(level: Int) -> Bool { - synchronized(self) { - scopes.first { $0.level == level } != nil - } + static let shared = ScopeStack() + + private var scopes = [LogScope]() + + func exists(level: Int) -> Bool { + synchronized(self) { + scopes.first { $0.level == level } != nil } - - func append(_ scope: LogScope, closure: () -> Void) { - synchronized(self) { - guard scopes.contains(scope) == false else { - return - } - let maxLevel = scopes.map{$0.level}.max() ?? 0 - scope.level = maxLevel + 1 - scopes.append(scope) - closure() - } + } + + func append(_ scope: LogScope, closure: () -> Void) { + synchronized(self) { + guard scopes.contains(scope) == false else { + return + } + let maxLevel = scopes.map{$0.level}.max() ?? 0 + scope.level = maxLevel + 1 + scopes.append(scope) + closure() } - - func remove(_ scope: LogScope, closure: () -> Void) { - synchronized(self) { - guard let index = scopes.firstIndex(of: scope) else { - return - } - closure() - scopes.remove(at: index) - scope.level = 0 - } + } + + func remove(_ scope: LogScope, closure: () -> Void) { + synchronized(self) { + guard let index = scopes.firstIndex(of: scope) else { + return + } + closure() + scopes.remove(at: index) + scope.level = 0 } + } } /// An object that represents a scope triggered by the user. @@ -66,63 +66,63 @@ class ScopeStack { /// Scope provides a mechanism for grouping log messages. /// public class LogScope: LogProtocol { - @Atomic var time = Date() + @Atomic var time = Date() - var os_state = os_activity_scope_state_s() - - /// A global level in the stack. - @objc - public internal(set) var level: Int = 0 - - /// A time duration. - @Atomic public private(set) var duration: TimeInterval = 0 - - /// Scope name. - @objc - public let name: String - - init(name: String, logger: DLog, category: String, config: LogConfig, metadata: Metadata) { - self.name = name - super.init(logger: logger, category: category, config: config, metadata: metadata) - self._scope = self - } - - /// Start a scope. - /// - /// A scope can be created and then used for logging grouped log messages. - /// - /// let logger = DLog() - /// let scope = logger.scope("Auth") - /// scope.enter() - /// - /// scope.log("message") - /// ... - /// - /// scope.leave() - /// - @objc - public func enter() { - time = Date() - duration = 0 - logger.enter(scope: self) - } - - /// Finish a scope. - /// - /// A scope can be created and then used for logging grouped log messages. - /// - /// let logger = DLog() - /// let scope = logger.scope("Auth") - /// scope.enter() - /// - /// scope.log("message") - /// ... - /// - /// scope.leave() - /// - @objc - public func leave() { - duration = -time.timeIntervalSinceNow - logger.leave(scope: self) - } + var os_state = os_activity_scope_state_s() + + /// A global level in the stack. + @objc + public internal(set) var level: Int = 0 + + /// A time duration. + @Atomic public private(set) var duration: TimeInterval = 0 + + /// Scope name. + @objc + public let name: String + + init(name: String, logger: DLog, category: String, config: LogConfig, metadata: Metadata) { + self.name = name + super.init(logger: logger, category: category, config: config, metadata: metadata) + self._scope = self + } + + /// Start a scope. + /// + /// A scope can be created and then used for logging grouped log messages. + /// + /// let logger = DLog() + /// let scope = logger.scope("Auth") + /// scope.enter() + /// + /// scope.log("message") + /// ... + /// + /// scope.leave() + /// + @objc + public func enter() { + time = Date() + duration = 0 + logger.enter(scope: self) + } + + /// Finish a scope. + /// + /// A scope can be created and then used for logging grouped log messages. + /// + /// let logger = DLog() + /// let scope = logger.scope("Auth") + /// scope.enter() + /// + /// scope.log("message") + /// ... + /// + /// scope.leave() + /// + @objc + public func leave() { + duration = -time.timeIntervalSinceNow + logger.leave(scope: self) + } } diff --git a/Sources/DLog/Net.swift b/Sources/DLog/Net.swift index 656bb65..41278ec 100644 --- a/Sources/DLog/Net.swift +++ b/Sources/DLog/Net.swift @@ -29,32 +29,32 @@ import Foundation #if !os(watchOS) private class LogBuffer { - private static let linesCount = 1000 - - private var stack = [String]() - - var text: String? { - synchronized(self) { - stack.reduce(nil) { text, line in - "\(text ?? "")\(line)\n" - } - } - } - - func append(text: String) { - synchronized(self) { - stack.append(text) - if stack.count > Self.linesCount { - _ = stack.removeFirst() - } - } - } - - func clear() { - synchronized(self) { - stack.removeAll() - } - } + private static let linesCount = 1000 + + private var stack = [String]() + + var text: String? { + synchronized(self) { + stack.reduce(nil) { text, line in + "\(text ?? "")\(line)\n" + } + } + } + + func append(text: String) { + synchronized(self) { + stack.append(text) + if stack.count > Self.linesCount { + _ = stack.removeFirst() + } + } + } + + func clear() { + synchronized(self) { + stack.removeAll() + } + } } /// A target output that sends log messages to `NetConsole` service. @@ -62,151 +62,151 @@ private class LogBuffer { /// `NetConsole` service can be run from a command line on your machine and then the output connects and sends your log messages to it. /// public class Net : LogOutput { - private static let type = "_dlog._tcp" - private static let domain = "local." - - private let name: String - private let browser = NetServiceBrowser() - private var service: NetService? - private let queue = DispatchQueue(label: "NetOutput") - private var outputStream : OutputStream? - private let buffer = LogBuffer() - - var debug = false - - /// Creates `Net` output object. - /// - /// To connect to a specific instance of the service in your network you should provide an unique name to both `NetConsole` and `Net` output. - /// - /// let logger = DLog(Net(name: "MyNetConsole")) - /// - /// - Parameters: - /// - name: A name of `NetConsole` service (defaults to `"DLog"`) - /// - source: A source output (defaults to `.textColored`) - public init(name: String = "DLog", source: LogOutput = .textColored) { - self.name = name - - super.init(source: source) - - browser.delegate = self - browser.searchForServices(ofType: Self.type, inDomain: Self.domain) - } - - deinit { - outputStream?.close() - browser.stop() - } - - @discardableResult - private func send(_ text: String?, newline: Bool = true) -> String? { - if let str = text, !str.isEmpty { - queue.async { - if let stream = self.outputStream { - let data = str + (newline ? "\n" : "") - stream.write(data, maxLength: data.lengthOfBytes(using: .utf8)) - } - else { - self.buffer.append(text: str) - } - } - } - - return text - } - - // Log debug messages - private func log(_ text: String) { - guard debug else { return } - print("[NetOutput] \(text)") - } - - // MARK: - LogOutput - - override func log(item: LogItem) -> String? { - send(super.log(item: item)) - } - - override func scopeEnter(scope: LogScope) -> String? { - send(super.scopeEnter(scope: scope)) - } - - override func scopeLeave(scope: LogScope) -> String? { - send(super.scopeLeave(scope: scope)) - } - - override func intervalEnd(interval: LogInterval) -> String? { - send(super.intervalEnd(interval: interval)) - } + private static let type = "_dlog._tcp" + private static let domain = "local." + + private let name: String + private let browser = NetServiceBrowser() + private var service: NetService? + private let queue = DispatchQueue(label: "NetOutput") + private var outputStream : OutputStream? + private let buffer = LogBuffer() + + var debug = false + + /// Creates `Net` output object. + /// + /// To connect to a specific instance of the service in your network you should provide an unique name to both `NetConsole` and `Net` output. + /// + /// let logger = DLog(Net(name: "MyNetConsole")) + /// + /// - Parameters: + /// - name: A name of `NetConsole` service (defaults to `"DLog"`) + /// - source: A source output (defaults to `.textColored`) + public init(name: String = "DLog", source: LogOutput = .textColored) { + self.name = name + + super.init(source: source) + + browser.delegate = self + browser.searchForServices(ofType: Self.type, inDomain: Self.domain) + } + + deinit { + outputStream?.close() + browser.stop() + } + + @discardableResult + private func send(_ text: String?, newline: Bool = true) -> String? { + if let str = text, !str.isEmpty { + queue.async { + if let stream = self.outputStream { + let data = str + (newline ? "\n" : "") + stream.write(data, maxLength: data.lengthOfBytes(using: .utf8)) + } + else { + self.buffer.append(text: str) + } + } + } + + return text + } + + // Log debug messages + private func log(_ text: String) { + guard debug else { return } + print("[NetOutput] \(text)") + } + + // MARK: - LogOutput + + override func log(item: LogItem) -> String? { + send(super.log(item: item)) + } + + override func scopeEnter(scope: LogScope) -> String? { + send(super.scopeEnter(scope: scope)) + } + + override func scopeLeave(scope: LogScope) -> String? { + send(super.scopeLeave(scope: scope)) + } + + override func intervalEnd(interval: LogInterval) -> String? { + send(super.intervalEnd(interval: interval)) + } } extension Net : NetServiceBrowserDelegate { - - /// Tells the delegate that a search is commencing. - public func netServiceBrowserWillSearch(_ browser: NetServiceBrowser) { - log("Begin search name:'\(name)', type:'\(Self.type)', domain:'\(Self.domain)'") - } - - /// Tells the delegate that a search was stopped. - public func netServiceBrowserDidStopSearch(_ browser: NetServiceBrowser) { - log("Stop search") - } - - /// Tells the delegate that a search was not successful. - public func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String : NSNumber]) { - log("Error: \(errorDict)") - } - - /// Tells the delegate the sender found a service. - public func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) { - guard service.name == self.name, - self.service == nil, - service.getInputStream(nil, outputStream: &outputStream) - else { return } - - log("Connected") - - self.service = service - - CFWriteStreamSetDispatchQueue(outputStream, queue) - outputStream?.delegate = self - outputStream?.open() - } - - /// Tells the delegate a service has disappeared or has become unavailable. - public func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) { - guard self.service == service else { return } - - outputStream?.close() - outputStream = nil - self.service = nil - - log("Disconnected") - } + + /// Tells the delegate that a search is commencing. + public func netServiceBrowserWillSearch(_ browser: NetServiceBrowser) { + log("Begin search name:'\(name)', type:'\(Self.type)', domain:'\(Self.domain)'") + } + + /// Tells the delegate that a search was stopped. + public func netServiceBrowserDidStopSearch(_ browser: NetServiceBrowser) { + log("Stop search") + } + + /// Tells the delegate that a search was not successful. + public func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String : NSNumber]) { + log("Error: \(errorDict)") + } + + /// Tells the delegate the sender found a service. + public func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) { + guard service.name == self.name, + self.service == nil, + service.getInputStream(nil, outputStream: &outputStream) + else { return } + + log("Connected") + + self.service = service + + CFWriteStreamSetDispatchQueue(outputStream, queue) + outputStream?.delegate = self + outputStream?.open() + } + + /// Tells the delegate a service has disappeared or has become unavailable. + public func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) { + guard self.service == service else { return } + + outputStream?.close() + outputStream = nil + self.service = nil + + log("Disconnected") + } } extension Net : StreamDelegate { - - /// The delegate receives this message when a given event has occurred on a given stream. - public func stream(_ aStream: Stream, handle eventCode: Stream.Event) { - switch eventCode { - case .openCompleted: - log("Output stream is opened") - - case .hasSpaceAvailable: - if let text = buffer.text { - send(text, newline: false) - buffer.clear() - } - - case .errorOccurred: - if let error = aStream.streamError { - log("Error: \(error.localizedDescription)") - } - - default: - break - } - } + + /// The delegate receives this message when a given event has occurred on a given stream. + public func stream(_ aStream: Stream, handle eventCode: Stream.Event) { + switch eventCode { + case .openCompleted: + log("Output stream is opened") + + case .hasSpaceAvailable: + if let text = buffer.text { + send(text, newline: false) + buffer.clear() + } + + case .errorOccurred: + if let error = aStream.streamError { + log("Error: \(error.localizedDescription)") + } + + default: + break + } + } } #endif diff --git a/Sources/DLog/OSLog.swift b/Sources/DLog/OSLog.swift index e0ebb4a..a674829 100644 --- a/Sources/DLog/OSLog.swift +++ b/Sources/DLog/OSLog.swift @@ -35,102 +35,102 @@ import os.activity /// retrieve log information such as: `Console` and `Instruments` apps, command line tool `"log"` etc. /// public class OSLog : LogOutput { + + private static let types: [LogType : OSLogType] = [ + .log : .default, + // Debug + .trace : .debug, + .debug : .debug, + // Info + .info : .info, + // Error + .warning : .error, + .error : .error, + // Fault + .assert : .fault, + .fault : .fault, + ] + + private var subsystem: String + private var logs = [String : os.OSLog]() + + /// Creates`OSlog` output object. + /// + /// To create OSLog you can use subsystem strings that identify major functional areas of your app, and you specify + /// them in reverse DNS notation—for example, com.your_company.your_subsystem_name. + /// + /// let logger = DLog(OSLog(subsystem: "com.myapp.logger")) + /// + /// - Parameters: + /// - subsystem: An identifier string, in reverse DNS notation, that represents the subsystem that’s performing + /// logging (defaults to `"com.dlog.logger"`). + /// - source: A source output (defaults to `.textPlain`) + /// + public init(subsystem: String = "com.dlog.logger", source: LogOutput = .textPlain) { + self.subsystem = subsystem - private static let types: [LogType : OSLogType] = [ - .log : .default, - // Debug - .trace : .debug, - .debug : .debug, - // Info - .info : .info, - // Error - .warning : .error, - .error : .error, - // Fault - .assert : .fault, - .fault : .fault, - ] - - private var subsystem: String - private var logs = [String : os.OSLog]() - - /// Creates`OSlog` output object. - /// - /// To create OSLog you can use subsystem strings that identify major functional areas of your app, and you specify - /// them in reverse DNS notation—for example, com.your_company.your_subsystem_name. - /// - /// let logger = DLog(OSLog(subsystem: "com.myapp.logger")) - /// - /// - Parameters: - /// - subsystem: An identifier string, in reverse DNS notation, that represents the subsystem that’s performing - /// logging (defaults to `"com.dlog.logger"`). - /// - source: A source output (defaults to `.textPlain`) - /// - public init(subsystem: String = "com.dlog.logger", source: LogOutput = .textPlain) { - self.subsystem = subsystem - - super.init(source: source) - } - - private func oslog(category: String) -> os.OSLog { - synchronized(self) { - if let log = logs[category] { - return log - } - let log = os.OSLog(subsystem: subsystem, category: category) - logs[category] = log - return log - } - } - - // MARK: - LogOutput - - override func log(item: LogItem) -> String? { - let log = oslog(category: item.category) - - let location = "<\(item.fileName):\(item.line)>" - - assert(Self.types[item.type] != nil) - let type = Self.types[item.type]! - os_log("%{public}@ %{public}@", dso: Dynamic.dso, log: log, type: type, location, item.text) - - return super.log(item: item) - } - - override func scopeEnter(scope: LogScope) -> String? { - if let os_activity_current = Dynamic.OS_ACTIVITY_CURRENT { - let activity = _os_activity_create(Dynamic.dso, strdup(scope.name), os_activity_current, OS_ACTIVITY_FLAG_DEFAULT) - os_activity_scope_enter(activity, &scope.os_state) - } - return super.scopeEnter(scope: scope) - } - - override func scopeLeave(scope: LogScope) -> String? { - if Dynamic.OS_ACTIVITY_CURRENT != nil { - os_activity_scope_leave(&scope.os_state); - } - return super.scopeLeave(scope: scope) - } - - override func intervalBegin(interval: LogInterval) { - super.intervalBegin(interval: interval) - - let log = oslog(category: interval.category) - if interval.signpostID == nil { - interval.signpostID = OSSignpostID(log: log) - } - - if let name = interval.staticName { - os_signpost(.begin, log: log, name: name, signpostID: interval.signpostID!) - } - } - - override func intervalEnd(interval: LogInterval) -> String? { - let log = oslog(category: interval.category) - - if let name = interval.staticName { - os_signpost(.end, log: log, name: name, signpostID: interval.signpostID!) - } - return super.intervalEnd(interval: interval) - } + super.init(source: source) + } + + private func oslog(category: String) -> os.OSLog { + synchronized(self) { + if let log = logs[category] { + return log + } + let log = os.OSLog(subsystem: subsystem, category: category) + logs[category] = log + return log + } + } + + // MARK: - LogOutput + + override func log(item: LogItem) -> String? { + let log = oslog(category: item.category) + + let location = "<\(item.fileName):\(item.line)>" + + assert(Self.types[item.type] != nil) + let type = Self.types[item.type]! + os_log("%{public}@ %{public}@", dso: Dynamic.dso, log: log, type: type, location, item.text) + + return super.log(item: item) + } + + override func scopeEnter(scope: LogScope) -> String? { + if let os_activity_current = Dynamic.OS_ACTIVITY_CURRENT { + let activity = _os_activity_create(Dynamic.dso, strdup(scope.name), os_activity_current, OS_ACTIVITY_FLAG_DEFAULT) + os_activity_scope_enter(activity, &scope.os_state) + } + return super.scopeEnter(scope: scope) + } + + override func scopeLeave(scope: LogScope) -> String? { + if Dynamic.OS_ACTIVITY_CURRENT != nil { + os_activity_scope_leave(&scope.os_state); + } + return super.scopeLeave(scope: scope) + } + + override func intervalBegin(interval: LogInterval) { + super.intervalBegin(interval: interval) + + let log = oslog(category: interval.category) + if interval.signpostID == nil { + interval.signpostID = OSSignpostID(log: log) + } + + if let name = interval.staticName { + os_signpost(.begin, log: log, name: name, signpostID: interval.signpostID!) + } + } + + override func intervalEnd(interval: LogInterval) -> String? { + let log = oslog(category: interval.category) + + if let name = interval.staticName { + os_signpost(.end, log: log, name: name, signpostID: interval.signpostID!) + } + return super.intervalEnd(interval: interval) + } } diff --git a/Sources/DLog/Standard.swift b/Sources/DLog/Standard.swift index 14d2716..b5a1f4c 100644 --- a/Sources/DLog/Standard.swift +++ b/Sources/DLog/Standard.swift @@ -29,46 +29,46 @@ import Foundation /// A target output that can output text messages to POSIX streams. /// public class Standard : LogOutput { - - let stream: UnsafeMutablePointer - - /// Creates `Standard` output object. - /// - /// let logger = DLog(Standard()) - /// logger.info("It's standard output") - /// - /// - Parameters: - /// - stream: POSIX stream: `Darwin.stdout`, `Darwin.stderr`. - /// - source: A source output (defaults to `.textPlain`). - /// - public init(stream: UnsafeMutablePointer = Darwin.stdout, source: LogOutput = .textPlain) { - self.stream = stream - - super.init(source: source) - } - - private func echo(_ text: String?) -> String? { - if let str = text, !str.isEmpty { - fputs(str + "\n", stream) - } - return text - } - - // MARK: - LogOutput - - override func log(item: LogItem) -> String? { - echo(super.log(item: item)) - } - - override func scopeEnter(scope: LogScope) -> String? { - echo(super.scopeEnter(scope: scope)) - } - - override func scopeLeave(scope: LogScope) -> String? { - echo(super.scopeLeave(scope: scope)) - } - - override func intervalEnd(interval: LogInterval) -> String? { - echo(super.intervalEnd(interval: interval)) - } + + let stream: UnsafeMutablePointer + + /// Creates `Standard` output object. + /// + /// let logger = DLog(Standard()) + /// logger.info("It's standard output") + /// + /// - Parameters: + /// - stream: POSIX stream: `Darwin.stdout`, `Darwin.stderr`. + /// - source: A source output (defaults to `.textPlain`). + /// + public init(stream: UnsafeMutablePointer = Darwin.stdout, source: LogOutput = .textPlain) { + self.stream = stream + + super.init(source: source) + } + + private func echo(_ text: String?) -> String? { + if let str = text, !str.isEmpty { + fputs(str + "\n", stream) + } + return text + } + + // MARK: - LogOutput + + override func log(item: LogItem) -> String? { + echo(super.log(item: item)) + } + + override func scopeEnter(scope: LogScope) -> String? { + echo(super.scopeEnter(scope: scope)) + } + + override func scopeLeave(scope: LogScope) -> String? { + echo(super.scopeLeave(scope: scope)) + } + + override func intervalEnd(interval: LogInterval) -> String? { + echo(super.intervalEnd(interval: interval)) + } } diff --git a/Sources/DLog/Text.swift b/Sources/DLog/Text.swift index 4079e97..471b2f0 100644 --- a/Sources/DLog/Text.swift +++ b/Sources/DLog/Text.swift @@ -28,81 +28,81 @@ import Foundation private enum ANSIEscapeCode: String { - case reset = "\u{001b}[0m" - case clear = "\u{001b}c" - - case bold = "\u{001b}[1m" - case dim = "\u{001b}[2m" - case underline = "\u{001b}[4m" - case blink = "\u{001b}[5m" - case reversed = "\u{001b}[7m" - - // 8 colors - case textBlack = "\u{001B}[30m" - case textRed = "\u{001B}[31m" - case textGreen = "\u{001B}[32m" - case textYellow = "\u{001B}[33m" - case textBlue = "\u{001B}[34m" - case textMagenta = "\u{001B}[35m" - case textCyan = "\u{001B}[36m" - case textWhite = "\u{001B}[37m" - - case backgroundBlack = "\u{001b}[40m" - case backgroundRed = "\u{001b}[41m" - case backgroundGreen = "\u{001b}[42m" - case backgroundYellow = "\u{001b}[43m" - case backgroundBlue = "\u{001b}[44m" - case backgroundMagenta = "\u{001b}[45m" - case backgroundCyan = "\u{001b}[46m" - case backgroundWhite = "\u{001b}[47m" + case reset = "\u{001b}[0m" + case clear = "\u{001b}c" + + case bold = "\u{001b}[1m" + case dim = "\u{001b}[2m" + case underline = "\u{001b}[4m" + case blink = "\u{001b}[5m" + case reversed = "\u{001b}[7m" + + // 8 colors + case textBlack = "\u{001B}[30m" + case textRed = "\u{001B}[31m" + case textGreen = "\u{001B}[32m" + case textYellow = "\u{001B}[33m" + case textBlue = "\u{001B}[34m" + case textMagenta = "\u{001B}[35m" + case textCyan = "\u{001B}[36m" + case textWhite = "\u{001B}[37m" + + case backgroundBlack = "\u{001b}[40m" + case backgroundRed = "\u{001b}[41m" + case backgroundGreen = "\u{001b}[42m" + case backgroundYellow = "\u{001b}[43m" + case backgroundBlue = "\u{001b}[44m" + case backgroundMagenta = "\u{001b}[45m" + case backgroundCyan = "\u{001b}[46m" + case backgroundWhite = "\u{001b}[47m" } fileprivate extension String { - func color(_ codes: [ANSIEscapeCode]) -> String { - return codes.map { $0.rawValue }.joined() + self + ANSIEscapeCode.reset.rawValue - } - - func color(_ code: ANSIEscapeCode) -> String { - return color([code]) - } - - func trimTrailingWhitespace() -> String { - replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression) - } + func color(_ codes: [ANSIEscapeCode]) -> String { + return codes.map { $0.rawValue }.joined() + self + ANSIEscapeCode.reset.rawValue + } + + func color(_ code: ANSIEscapeCode) -> String { + return color([code]) + } + + func trimTrailingWhitespace() -> String { + replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression) + } } private extension LogType { - static let icons: [LogType : String] = [ - .log : "💬", - .trace : "#️⃣", - .debug : "▶️", - .info : "✅", - .warning: "⚠️", - .error : "⚠️", - .assert : "🅰️", - .fault : "🆘", - .interval : "🕒", - ] - - var icon: String { - Self.icons[self]! - } - - static let titles: [LogType : String] = [ - .log : "LOG", - .trace : "TRACE", - .debug : "DEBUG", - .info : "INFO", - .warning : "WARNING", - .error : "ERROR", - .assert : "ASSERT", - .fault : "FAULT", - .interval : "INTERVAL", - ] - - var title: String { - Self.titles[self]! - } + static let icons: [LogType : String] = [ + .log : "💬", + .trace : "#️⃣", + .debug : "▶️", + .info : "✅", + .warning: "⚠️", + .error : "⚠️", + .assert : "🅰️", + .fault : "🆘", + .interval : "🕒", + ] + + var icon: String { + Self.icons[self]! + } + + static let titles: [LogType : String] = [ + .log : "LOG", + .trace : "TRACE", + .debug : "DEBUG", + .info : "INFO", + .warning : "WARNING", + .error : "ERROR", + .assert : "ASSERT", + .fault : "FAULT", + .interval : "INTERVAL", + ] + + var title: String { + Self.titles[self]! + } } /// A source output that generates text representation of log messages. @@ -110,191 +110,191 @@ private extension LogType { /// It doesn’t deliver text to any target outputs (stdout, file etc.) and usually other outputs use it. /// public class Text : LogOutput { - - private struct Tag { - let textColor: ANSIEscapeCode - let colors: [ANSIEscapeCode] - } - - private static let tags: [LogType : Tag] = [ - .log : Tag(textColor: .textWhite, colors: [.backgroundWhite, .textBlack]), - .info : Tag(textColor: .textGreen, colors: [.backgroundGreen, .textWhite]), - .trace : Tag(textColor: .textCyan, colors: [.backgroundCyan, .textBlack]), - .debug : Tag(textColor: .textCyan, colors: [.backgroundCyan, .textBlack]), - .warning : Tag(textColor: .textYellow, colors: [.backgroundYellow, .textBlack]), - .error : Tag(textColor: .textYellow, colors: [.backgroundYellow, .textBlack]), - .fault : Tag(textColor: .textRed, colors: [.backgroundRed, .textWhite, .blink]), - .assert : Tag(textColor: .textRed, colors: [.backgroundRed, .textWhite]), - .interval : Tag(textColor: .textGreen, colors: [.backgroundGreen, .textBlack]), - ] - - /// Style of text to output. - public enum Style { - /// Universal plain text. - case plain - - /// Text with type icons for info, debug etc. (useful for XCode console). - case emoji - - /// Colored text with ANSI escape codes (useful for Terminal and files). - case colored - } - - private let style: Style - - /// Creates `Text` source output object. - /// - /// let logger = DLog(Text(style: .emoji)) - /// logger.info("It's emoji text") - /// - /// - Parameters: - /// - style: Style of text to output (defaults to `.plain`). - /// - public init(style: Style = .plain) { - self.style = style - - super.init(source: nil) - } - - private static let dateFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "HH:mm:ss.SSS" - return dateFormatter - }() - - private func logPrefix(items: [(LogOptions, () -> String)], options: LogOptions) -> String { - items.compactMap { - guard options.contains($0.0) else { - return nil - } - let text = $0.1() - return text.trimTrailingWhitespace() + + private struct Tag { + let textColor: ANSIEscapeCode + let colors: [ANSIEscapeCode] + } + + private static let tags: [LogType : Tag] = [ + .log : Tag(textColor: .textWhite, colors: [.backgroundWhite, .textBlack]), + .info : Tag(textColor: .textGreen, colors: [.backgroundGreen, .textWhite]), + .trace : Tag(textColor: .textCyan, colors: [.backgroundCyan, .textBlack]), + .debug : Tag(textColor: .textCyan, colors: [.backgroundCyan, .textBlack]), + .warning : Tag(textColor: .textYellow, colors: [.backgroundYellow, .textBlack]), + .error : Tag(textColor: .textYellow, colors: [.backgroundYellow, .textBlack]), + .fault : Tag(textColor: .textRed, colors: [.backgroundRed, .textWhite, .blink]), + .assert : Tag(textColor: .textRed, colors: [.backgroundRed, .textWhite]), + .interval : Tag(textColor: .textGreen, colors: [.backgroundGreen, .textBlack]), + ] + + /// Style of text to output. + public enum Style { + /// Universal plain text. + case plain + + /// Text with type icons for info, debug etc. (useful for XCode console). + case emoji + + /// Colored text with ANSI escape codes (useful for Terminal and files). + case colored + } + + private let style: Style + + /// Creates `Text` source output object. + /// + /// let logger = DLog(Text(style: .emoji)) + /// logger.info("It's emoji text") + /// + /// - Parameters: + /// - style: Style of text to output (defaults to `.plain`). + /// + public init(style: Style = .plain) { + self.style = style + + super.init(source: nil) + } + + private static let dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH:mm:ss.SSS" + return dateFormatter + }() + + private func logPrefix(items: [(LogOptions, () -> String)], options: LogOptions) -> String { + items.compactMap { + guard options.contains($0.0) else { + return nil + } + let text = $0.1() + return text.trimTrailingWhitespace() + } + .joinedCompact() + } + + private func textMessage(item: LogItem) -> String { + var sign = { "\(item.config.sign)" } + var time = { Self.dateFormatter.string(from: item.time) } + var level = { String(format: "[%02d]", item.scope?.level ?? 0) } + var category = { "[\(item.category)]" } + let padding: () -> String = { + guard let scope = item.scope, scope.level > 0 else { return "" } + return (1...scope.level) + .map { + ScopeStack.shared.exists(level: $0) + ? ($0 == scope.level) ? "├ " : "│ " + : " " } - .joinedCompact() - } - - private func textMessage(item: LogItem) -> String { - var sign = { "\(item.config.sign)" } - var time = { Self.dateFormatter.string(from: item.time) } - var level = { String(format: "[%02d]", item.scope?.level ?? 0) } - var category = { "[\(item.category)]" } - let padding: () -> String = { - guard let scope = item.scope, scope.level > 0 else { return "" } - return (1...scope.level) - .map { - ScopeStack.shared.exists(level: $0) - ? ($0 == scope.level) ? "├ " : "│ " - : " " - } - .joined() - } - var type = { "[\(item.type.title)]" } - var location = { "<\(item.fileName):\(item.line)>" } - var metadata = { item.metadata.json(parenthesis: true) } - var text = item.text - - switch style { - case .plain: - break - - case .colored: - assert(Self.tags[item.type] != nil) - let tag = Self.tags[item.type]! - - sign = { "\(item.config.sign)".color(.dim) } - time = { Self.dateFormatter.string(from: item.time).color(.dim) } - level = { String(format: "[%02d]", item.scope?.level ?? 0).color(.dim) } - category = { item.category.color(.textBlue) } - type = { " \(item.type.title) ".color(tag.colors) } - location = { "<\(item.fileName):\(item.line)>".color([.dim, tag.textColor]) } - metadata = { item.metadata.json(parenthesis: true).color(.dim) } - text = text.color(tag.textColor) - - case .emoji: - type = { "\(item.type.icon) [\(item.type.title)]" } - } - - let items: [(LogOptions, () -> String)] = [ - (.sign, sign), - (.time, time), - (.level, level), - (.category, category), - (.padding, padding), - (.type, type), - (.location, location), - (.metadata, metadata) - ] - let prefix = logPrefix(items: items, options: item.config.options) - return [prefix, text].joinedCompact() - } - - private func textScope(scope: LogScope) -> String { - let start = scope.duration == 0 - - var sign = { "\(scope.config.sign)" } - var time = start - ? Self.dateFormatter.string(from: scope.time) - : Self.dateFormatter.string(from: scope.time.addingTimeInterval(scope.duration)) - let ms = !start ? "(\(stringFromTimeInterval(scope.duration)))" : nil - var category = { "[\(scope.category)]" } - var level = { String(format: "[%02d]", scope.level) } - let padding: () -> String = { - let text = (1.. String)] = [ - (.sign, sign), - (.time, { time }), - (.level, level), - (.category, category), - (.padding, padding), - ] - let prefix = logPrefix(items: items, options: scope.config.options) - return prefix.isEmpty ? text : "\(prefix) \(text)" - } - - // MARK: - LogOutput - - override func log(item: LogItem) -> String? { - super.log(item: item) - return textMessage(item: item) - } - - override func scopeEnter(scope: LogScope) -> String? { - super.scopeEnter(scope: scope) - - return textScope(scope: scope) - } - - override func scopeLeave(scope: LogScope) -> String? { - super.scopeLeave(scope: scope) - - return textScope(scope: scope) - } - - override func intervalBegin(interval: LogInterval) { - super.intervalBegin(interval: interval) - } - - override func intervalEnd(interval: LogInterval) -> String? { - super.intervalEnd(interval: interval) - - return textMessage(item: interval) - } + .joined() + } + var type = { "[\(item.type.title)]" } + var location = { "<\(item.fileName):\(item.line)>" } + var metadata = { item.metadata.json(parenthesis: true) } + var text = item.text + + switch style { + case .plain: + break + + case .colored: + assert(Self.tags[item.type] != nil) + let tag = Self.tags[item.type]! + + sign = { "\(item.config.sign)".color(.dim) } + time = { Self.dateFormatter.string(from: item.time).color(.dim) } + level = { String(format: "[%02d]", item.scope?.level ?? 0).color(.dim) } + category = { item.category.color(.textBlue) } + type = { " \(item.type.title) ".color(tag.colors) } + location = { "<\(item.fileName):\(item.line)>".color([.dim, tag.textColor]) } + metadata = { item.metadata.json(parenthesis: true).color(.dim) } + text = text.color(tag.textColor) + + case .emoji: + type = { "\(item.type.icon) [\(item.type.title)]" } + } + + let items: [(LogOptions, () -> String)] = [ + (.sign, sign), + (.time, time), + (.level, level), + (.category, category), + (.padding, padding), + (.type, type), + (.location, location), + (.metadata, metadata) + ] + let prefix = logPrefix(items: items, options: item.config.options) + return [prefix, text].joinedCompact() + } + + private func textScope(scope: LogScope) -> String { + let start = scope.duration == 0 + + var sign = { "\(scope.config.sign)" } + var time = start + ? Self.dateFormatter.string(from: scope.time) + : Self.dateFormatter.string(from: scope.time.addingTimeInterval(scope.duration)) + let ms = !start ? "(\(stringFromTimeInterval(scope.duration)))" : nil + var category = { "[\(scope.category)]" } + var level = { String(format: "[%02d]", scope.level) } + let padding: () -> String = { + let text = (1.. String)] = [ + (.sign, sign), + (.time, { time }), + (.level, level), + (.category, category), + (.padding, padding), + ] + let prefix = logPrefix(items: items, options: scope.config.options) + return prefix.isEmpty ? text : "\(prefix) \(text)" + } + + // MARK: - LogOutput + + override func log(item: LogItem) -> String? { + super.log(item: item) + return textMessage(item: item) + } + + override func scopeEnter(scope: LogScope) -> String? { + super.scopeEnter(scope: scope) + + return textScope(scope: scope) + } + + override func scopeLeave(scope: LogScope) -> String? { + super.scopeLeave(scope: scope) + + return textScope(scope: scope) + } + + override func intervalBegin(interval: LogInterval) { + super.intervalBegin(interval: interval) + } + + override func intervalEnd(interval: LogInterval) -> String? { + super.intervalEnd(interval: interval) + + return textMessage(item: interval) + } } diff --git a/Sources/DLog/Trace.swift b/Sources/DLog/Trace.swift index ee454bd..751dc3d 100644 --- a/Sources/DLog/Trace.swift +++ b/Sources/DLog/Trace.swift @@ -29,135 +29,135 @@ import Foundation // MARK: - Thread fileprivate extension QualityOfService { - - static let names: [QualityOfService : String] = [ - .userInteractive : "userInteractive", - .userInitiated: "userInitiated", - .utility: "utility", - .background: "background", - .default : "default", - ] - - var description: String { - precondition(Self.names[self] != nil) - return Self.names[self]! - } + + static let names: [QualityOfService : String] = [ + .userInteractive : "userInteractive", + .userInitiated: "userInitiated", + .utility: "utility", + .background: "background", + .default : "default", + ] + + var description: String { + precondition(Self.names[self] != nil) + return Self.names[self]! + } } fileprivate extension Thread { - - // {number = 1, name = main} - static let regexThread = try! NSRegularExpression(pattern: "number = ([0-9]+), name = ([^}]+)") - - func dict(config: ThreadConfig) -> [String : Any] { - var number = "" - var name = "" - let nsString = description as NSString - if let match = Self.regexThread.matches(in: description, options: [], range: NSMakeRange(0, nsString.length)).first, - match.numberOfRanges == 3 { - number = nsString.substring(with: match.range(at: 1)) - name = nsString.substring(with: match.range(at: 2)) - if name == "(null)" { - name = "" - } - } - - let items: [(ThreadOptions, String, () -> Any)] = [ - (.number, "number", { number }), - (.name, "name", { name }), - (.priority, "priority", { self.threadPriority }), - (.qos, "qos", { "\(self.qualityOfService.description)" }), - (.stackSize, "stackSize", { "\(ByteCountFormatter.string(fromByteCount: Int64(self.stackSize), countStyle: .memory))" }), - ] - - let dict = dictionary(from: items, options: config.options) - return dict - } + + // {number = 1, name = main} + static let regexThread = try! NSRegularExpression(pattern: "number = ([0-9]+), name = ([^}]+)") + + func dict(config: ThreadConfig) -> [String : Any] { + var number = "" + var name = "" + let nsString = description as NSString + if let match = Self.regexThread.matches(in: description, options: [], range: NSMakeRange(0, nsString.length)).first, + match.numberOfRanges == 3 { + number = nsString.substring(with: match.range(at: 1)) + name = nsString.substring(with: match.range(at: 2)) + if name == "(null)" { + name = "" + } + } + + let items: [(ThreadOptions, String, () -> Any)] = [ + (.number, "number", { number }), + (.name, "name", { name }), + (.priority, "priority", { self.threadPriority }), + (.qos, "qos", { "\(self.qualityOfService.description)" }), + (.stackSize, "stackSize", { "\(ByteCountFormatter.string(fromByteCount: Int64(self.stackSize), countStyle: .memory))" }), + ] + + let dict = dictionary(from: items, options: config.options) + return dict + } } // MARK: - Stack fileprivate func demangle(_ mangled: String) -> String? { - guard mangled.hasPrefix("$s") else { return nil } - - if let cString = Dynamic.swift_demangle?(mangled, mangled.count, nil, nil, 0) { - defer { cString.deallocate() } - return String(cString: cString) - } - return nil + guard mangled.hasPrefix("$s") else { return nil } + + if let cString = Dynamic.swift_demangle?(mangled, mangled.count, nil, nil, 0) { + defer { cString.deallocate() } + return String(cString: cString) + } + return nil } fileprivate func stack(_ addresses: ArraySlice, config: StackConfig) -> [[String : Any]] { - var info = dl_info() - - return addresses - .compactMap { address -> (String, UInt, String, UInt)? in - let pointer = UnsafeRawPointer(bitPattern: address.uintValue) - guard dladdr(pointer, &info) != 0 else { - return nil - } - - let fname = String(validatingUTF8: info.dli_fname)! - let module = (fname as NSString).lastPathComponent - - let sname = String(validatingUTF8: info.dli_sname)! - let name = demangle(sname) ?? sname - - let offset = address.uintValue - UInt(bitPattern: info.dli_saddr) - - return (module, address.uintValue, name, offset) - } - .prefix(config.depth > 0 ? config.depth : addresses.count) - .enumerated() - .map { item in - let items: [(StackOptions, String, () -> Any)] = [ - (.module, "module", { "\(item.element.0)" }), - (.address, "address", { String(format:"0x%016llx", item.element.1) }), - (.symbols, "symbols", { "\(item.element.2)" }), - (.offset, "offset", { "\(item.element.3)" }), - (.frame, "frame", { "\(item.offset)" }), - ] - let dict = dictionary(from: items, options: config.options) - return dict - } + var info = dl_info() + + return addresses + .compactMap { address -> (String, UInt, String, UInt)? in + let pointer = UnsafeRawPointer(bitPattern: address.uintValue) + guard dladdr(pointer, &info) != 0 else { + return nil + } + + let fname = String(validatingUTF8: info.dli_fname)! + let module = (fname as NSString).lastPathComponent + + let sname = String(validatingUTF8: info.dli_sname)! + let name = demangle(sname) ?? sname + + let offset = address.uintValue - UInt(bitPattern: info.dli_saddr) + + return (module, address.uintValue, name, offset) + } + .prefix(config.depth > 0 ? config.depth : addresses.count) + .enumerated() + .map { item in + let items: [(StackOptions, String, () -> Any)] = [ + (.module, "module", { "\(item.element.0)" }), + (.address, "address", { String(format:"0x%016llx", item.element.1) }), + (.symbols, "symbols", { "\(item.element.2)" }), + (.offset, "offset", { "\(item.element.3)" }), + (.frame, "frame", { "\(item.offset)" }), + ] + let dict = dictionary(from: items, options: config.options) + return dict + } } func traceInfo(text: String?, function: String, addresses: ArraySlice, traceConfig: TraceConfig) -> String { - let items: [(TraceOptions, String, () -> Any)] = [ - (.function, "func", { function }), - (.queue, "queue", { "\(String(cString: __dispatch_queue_get_label(nil)))" }), - (.thread, "thread", { Thread.current.dict(config: traceConfig.threadConfig) }), - (.stack, "stack", { stack(addresses, config: traceConfig.stackConfig) }), - ] - let dict = dictionary(from: items, options: traceConfig.options) - let pretty = traceConfig.style == .pretty - return [dict.json(pretty: pretty), text ?? ""].joinedCompact() + let items: [(TraceOptions, String, () -> Any)] = [ + (.function, "func", { function }), + (.queue, "queue", { "\(String(cString: __dispatch_queue_get_label(nil)))" }), + (.thread, "thread", { Thread.current.dict(config: traceConfig.threadConfig) }), + (.stack, "stack", { stack(addresses, config: traceConfig.stackConfig) }), + ] + let dict = dictionary(from: items, options: traceConfig.options) + let pretty = traceConfig.style == .pretty + return [dict.json(pretty: pretty), text ?? ""].joinedCompact() } extension Array where Element == String { - func joinedCompact() -> String { - compactMap { $0.isEmpty ? nil : $0 } - .joined(separator: " ") - } + func joinedCompact() -> String { + compactMap { $0.isEmpty ? nil : $0 } + .joined(separator: " ") + } } func dictionary(from items: [(Option, String, () -> Any)], options: Option) -> [String : Any] { - let keyValues: [(String, Any)] = items - .compactMap { - guard options.contains($0.0 as! Option.Element) else { - return nil - } - let key = $0.1 - let value = $0.2() - - if let text = value as? String, text.isEmpty { - return nil - } - else if let dict = value as? [String : Any], dict.isEmpty { - return nil - } - - return (key, value) - } - return Dictionary(uniqueKeysWithValues: keyValues) + let keyValues: [(String, Any)] = items + .compactMap { + guard options.contains($0.0 as! Option.Element) else { + return nil + } + let key = $0.1 + let value = $0.2() + + if let text = value as? String, text.isEmpty { + return nil + } + else if let dict = value as? [String : Any], dict.isEmpty { + return nil + } + + return (key, value) + } + return Dictionary(uniqueKeysWithValues: keyValues) } diff --git a/Sources/DLogObjC/DLog.m b/Sources/DLogObjC/DLog.m index 8c0bd2b..c835f52 100644 --- a/Sources/DLogObjC/DLog.m +++ b/Sources/DLogObjC/DLog.m @@ -3,63 +3,63 @@ @implementation LogProtocol (PropertyWrapper) - (LogBlock)log { - return ^(NSString* text, NSString* file, NSString* func, NSUInteger line){ - return [self log:^{ return [[LogMessage alloc] initWithStringLiteral:text]; } file:file function:func line:line]; - }; + return ^(NSString* text, NSString* file, NSString* func, NSUInteger line){ + return [self log:^{ return [[LogMessage alloc] initWithStringLiteral:text]; } file:file function:func line:line]; + }; } - (TraceBlock)trace { - return ^(NSString* text, NSString* file, NSString* func, NSUInteger line, NSArray* addresses){ - return [self trace:^{ return [[LogMessage alloc] initWithStringLiteral:text]; } file:file function:func line:line addresses:addresses]; - }; + return ^(NSString* text, NSString* file, NSString* func, NSUInteger line, NSArray* addresses){ + return [self trace:^{ return [[LogMessage alloc] initWithStringLiteral:text]; } file:file function:func line:line addresses:addresses]; + }; } - (LogBlock)debug { - return ^(NSString* text, NSString* file, NSString* func, NSUInteger line){ - return [self debug:^{ return [[LogMessage alloc] initWithStringLiteral:text]; } file:file function:func line:line]; - }; + return ^(NSString* text, NSString* file, NSString* func, NSUInteger line){ + return [self debug:^{ return [[LogMessage alloc] initWithStringLiteral:text]; } file:file function:func line:line]; + }; } - (LogBlock)info { - return ^(NSString* text, NSString* file, NSString* func, NSUInteger line){ - return [self info:^{ return [[LogMessage alloc] initWithStringLiteral:text]; } file:file function:func line:line]; - }; + return ^(NSString* text, NSString* file, NSString* func, NSUInteger line){ + return [self info:^{ return [[LogMessage alloc] initWithStringLiteral:text]; } file:file function:func line:line]; + }; } - (LogBlock)warning { - return ^(NSString* text, NSString* file, NSString* func, NSUInteger line){ - return [self warning:^{ return [[LogMessage alloc] initWithStringLiteral:text]; } file:file function:func line:line]; - }; + return ^(NSString* text, NSString* file, NSString* func, NSUInteger line){ + return [self warning:^{ return [[LogMessage alloc] initWithStringLiteral:text]; } file:file function:func line:line]; + }; } - (LogBlock)error { - return ^(NSString* text, NSString* file, NSString* func, NSUInteger line){ - return [self error:^{ return [[LogMessage alloc] initWithStringLiteral:text]; } file:file function:func line:line]; - }; + return ^(NSString* text, NSString* file, NSString* func, NSUInteger line){ + return [self error:^{ return [[LogMessage alloc] initWithStringLiteral:text]; } file:file function:func line:line]; + }; } - (AssertBlock)assertion { - return ^(BOOL condition, NSString* text, NSString* file, NSString* func, NSUInteger line){ - return [self assert:^{ return condition; } : ^{ return [[LogMessage alloc] initWithStringLiteral:text]; } file:file function:func line:line]; - }; + return ^(BOOL condition, NSString* text, NSString* file, NSString* func, NSUInteger line){ + return [self assert:^{ return condition; } : ^{ return [[LogMessage alloc] initWithStringLiteral:text]; } file:file function:func line:line]; + }; } - (LogBlock)fault { - return ^(NSString* text, NSString* file, NSString* func, NSUInteger line){ - return [self fault:^{ return [[LogMessage alloc] initWithStringLiteral:text]; } file:file function:func line:line]; - }; + return ^(NSString* text, NSString* file, NSString* func, NSUInteger line){ + return [self fault:^{ return [[LogMessage alloc] initWithStringLiteral:text]; } file:file function:func line:line]; + }; } - (ScopeBlock)scope { - return ^(NSString* name, NSString* file, NSString* func, NSUInteger line, void (^block)(LogScope*)){ - return [self scope:name metadata:nil file:file function:func line:line closure:block]; - }; + return ^(NSString* name, NSString* file, NSString* func, NSUInteger line, void (^block)(LogScope*)){ + return [self scope:name metadata:nil file:file function:func line:line closure:block]; + }; } - (IntervalBlock)interval { - return ^(NSString* name, NSString* file, NSString* func, NSUInteger line, void (^block)()){ - return [self intervalWithName:name file:file function:func line:line closure:block]; - }; + return ^(NSString* name, NSString* file, NSString* func, NSUInteger line, void (^block)()){ + return [self intervalWithName:name file:file function:func line:line closure:block]; + }; } @end diff --git a/Sources/NetConsole/Arguments.swift b/Sources/NetConsole/Arguments.swift index 8f7b3f0..94b7460 100644 --- a/Sources/NetConsole/Arguments.swift +++ b/Sources/NetConsole/Arguments.swift @@ -28,48 +28,48 @@ import Foundation class Arguments { - private var arguments = [String : String]() - - init() { - var key: String? - for param in CommandLine.arguments { - if param.hasPrefix("-") { - if key != nil { - arguments[key!] = "" - } - - key = param - - // Last - if param == CommandLine.arguments.last { - arguments[key!] = "" - } - } - else { - if key != nil { - arguments[key!] = param - key = nil - } - } - } - } - - func stringValue(forKeys keys: [String], defaultValue: String) -> String { - for key in keys { - if let value = arguments[key], !value.isEmpty { - return value - } - } - return defaultValue - } - - func boolValue(forKeys keys: [String], defaultValue: Bool = false) -> Bool { - for key in keys { - if arguments[key] != nil { - return true - } - } - return false - } - + private var arguments = [String : String]() + + init() { + var key: String? + for param in CommandLine.arguments { + if param.hasPrefix("-") { + if key != nil { + arguments[key!] = "" + } + + key = param + + // Last + if param == CommandLine.arguments.last { + arguments[key!] = "" + } + } + else { + if key != nil { + arguments[key!] = param + key = nil + } + } + } + } + + func stringValue(forKeys keys: [String], defaultValue: String) -> String { + for key in keys { + if let value = arguments[key], !value.isEmpty { + return value + } + } + return defaultValue + } + + func boolValue(forKeys keys: [String], defaultValue: Bool = false) -> Bool { + for key in keys { + if arguments[key] != nil { + return true + } + } + return false + } + } diff --git a/Sources/NetConsole/main.swift b/Sources/NetConsole/main.swift index 864b104..bf74179 100644 --- a/Sources/NetConsole/main.swift +++ b/Sources/NetConsole/main.swift @@ -28,117 +28,117 @@ import Foundation class Service : NSObject { - - enum ANSIEscapeCode: String { - case reset = "\u{001b}[0m" - case clear = "\u{001b}c" - - case bold = "\u{001b}[1m" - case dim = "\u{001b}[2m" - case underline = "\u{001b}[4m" - case blink = "\u{001b}[5m" - case reversed = "\u{001b}[7m" - } - - let name: String - let debug: Bool - let autoClear: Bool - - let service: NetService - var inputStream: InputStream? - - static let bufferSize = 1024 - var buffer = [UInt8](repeating: 0, count: bufferSize) - - deinit { - log("deinit") - } - - init(name: String, debug: Bool, autoClear: Bool) { - self.name = name - self.debug = debug - self.autoClear = autoClear - - service = NetService(domain: "local.", type:"_dlog._tcp.", name: name, port: 0) - super.init() - - service.delegate = self - service.publish(options: .listenForConnections) - } - - private func log(_ text: String) { - guard debug else { return } - - print("\(ANSIEscapeCode.dim.rawValue)[NetConsole]", text, ANSIEscapeCode.reset.rawValue) - } - - private func reject(inputStream: InputStream, outputStream: OutputStream) { - inputStream.open(); outputStream.open() - inputStream.close(); outputStream.close() - } + + enum ANSIEscapeCode: String { + case reset = "\u{001b}[0m" + case clear = "\u{001b}c" + + case bold = "\u{001b}[1m" + case dim = "\u{001b}[2m" + case underline = "\u{001b}[4m" + case blink = "\u{001b}[5m" + case reversed = "\u{001b}[7m" + } + + let name: String + let debug: Bool + let autoClear: Bool + + let service: NetService + var inputStream: InputStream? + + static let bufferSize = 1024 + var buffer = [UInt8](repeating: 0, count: bufferSize) + + deinit { + log("deinit") + } + + init(name: String, debug: Bool, autoClear: Bool) { + self.name = name + self.debug = debug + self.autoClear = autoClear + + service = NetService(domain: "local.", type:"_dlog._tcp.", name: name, port: 0) + super.init() + + service.delegate = self + service.publish(options: .listenForConnections) + } + + private func log(_ text: String) { + guard debug else { return } + + print("\(ANSIEscapeCode.dim.rawValue)[NetConsole]", text, ANSIEscapeCode.reset.rawValue) + } + + private func reject(inputStream: InputStream, outputStream: OutputStream) { + inputStream.open(); outputStream.open() + inputStream.close(); outputStream.close() + } } extension Service : NetServiceDelegate { - - func netServiceDidPublish(_ sender: NetService) { - log("Published name:'\(sender.name)', domain:'\(sender.domain)', type:'\(sender.type)', port: \(sender.port)") - } - - func netService(_ sender: NetService, didNotPublish errorDict: [String : NSNumber]) { - log("Error: \(errorDict)") - } - - func netService(_ sender: NetService, didAcceptConnectionWith inputStream: InputStream, outputStream: OutputStream) { - guard self.inputStream == nil else { - reject(inputStream: inputStream, outputStream: outputStream) - return - } - - self.inputStream = inputStream - inputStream.delegate = self - inputStream.schedule(in: .current, forMode: .default) - inputStream.open() - - if autoClear { - print(ANSIEscapeCode.clear.rawValue) - } - - log("Connected") - } + + func netServiceDidPublish(_ sender: NetService) { + log("Published name:'\(sender.name)', domain:'\(sender.domain)', type:'\(sender.type)', port: \(sender.port)") + } + + func netService(_ sender: NetService, didNotPublish errorDict: [String : NSNumber]) { + log("Error: \(errorDict)") + } + + func netService(_ sender: NetService, didAcceptConnectionWith inputStream: InputStream, outputStream: OutputStream) { + guard self.inputStream == nil else { + reject(inputStream: inputStream, outputStream: outputStream) + return + } + + self.inputStream = inputStream + inputStream.delegate = self + inputStream.schedule(in: .current, forMode: .default) + inputStream.open() + + if autoClear { + print(ANSIEscapeCode.clear.rawValue) + } + + log("Connected") + } } extension Service : StreamDelegate { - - func stream(_ aStream: Stream, handle eventCode: Stream.Event) { - switch eventCode { - case .openCompleted: - log("Input stream is opened") - - case .hasBytesAvailable: - guard let stream = inputStream else { return } - - while stream.hasBytesAvailable { - let count = stream.read(&buffer, maxLength: Self.bufferSize) - if count > 0 { - if let text = String(bytes: buffer[0.. 0 { + if let text = String(bytes: buffer[0..] [--auto-clear] [--debug] OPTIONS: -n, --name The name by which the service is identified to the network. The name must be unique and by default it equals - "DLog". If you pass the empty string (""), the system automatically advertises your service using the computer - name as the service name. + "DLog". If you pass the empty string (""), the system automatically advertises your service using the computer + name as the service name. -a, --auto-clear Clear a terminal on new connection. -d, --debug Enable debug messages. -h, --help Show help information. """ - ) - exit(EXIT_SUCCESS) + ) + exit(EXIT_SUCCESS) } var name = arguments.stringValue(forKeys: ["--name", "-n"], defaultValue: "DLog") diff --git a/Tests/DLogTests/DLogTests.swift b/Tests/DLogTests/DLogTests.swift index de9a8b9..8b82f8b 100644 --- a/Tests/DLogTests/DLogTests.swift +++ b/Tests/DLogTests/DLogTests.swift @@ -9,108 +9,108 @@ import Network // Locale: en_US extension NSLocale { - @objc - static let currentLocale = NSLocale(localeIdentifier: "en_US") + @objc + static let currentLocale = NSLocale(localeIdentifier: "en_US") } // Time zone: GMT // NSTimeZone.default = TimeZone(abbreviation: "GMT")! extension NSTimeZone { - @objc - static let defaultTimeZone = TimeZone(abbreviation: "GMT") + @objc + static let defaultTimeZone = TimeZone(abbreviation: "GMT") } /// String errors extension String : LocalizedError { - public var errorDescription: String? { return self } + public var errorDescription: String? { return self } } extension String { - func match(_ pattern: String) -> Bool { - self.range(of: pattern, options: [.regularExpression]) != nil - } + func match(_ pattern: String) -> Bool { + self.range(of: pattern, options: [.regularExpression]) != nil + } } extension DispatchSemaphore { - static func Lock() -> DispatchSemaphore { - return DispatchSemaphore(value: 0) - } - - static func Mutex() -> DispatchSemaphore { - return DispatchSemaphore(value: 1) - } + static func Lock() -> DispatchSemaphore { + return DispatchSemaphore(value: 0) + } + + static func Mutex() -> DispatchSemaphore { + return DispatchSemaphore(value: 1) + } } extension XCTestCase { - - func wait(count: Int, timeout: TimeInterval = 1, repeat r: Int = 1, name: String = #function, closure: ([XCTestExpectation]) -> Void) { - guard count > 0, r > 0 else { return } - - let exps = (0.. Void) { - wait(count: 1, timeout: timeout, name: name) { expectations in - closure(expectations[0]) - } - } + + func wait(count: Int, timeout: TimeInterval = 1, repeat r: Int = 1, name: String = #function, closure: ([XCTestExpectation]) -> Void) { + guard count > 0, r > 0 else { return } + + let exps = (0.. Void) { + wait(count: 1, timeout: timeout, name: name) { expectations in + closure(expectations[0]) + } + } } // MARK: - Utils func delay(_ sec: TimeInterval = 0.25) { - Thread.sleep(forTimeInterval: sec) + Thread.sleep(forTimeInterval: sec) } func asyncAfter(_ sec: Double = 0.25, closure: @escaping (() -> Void) ) { - DispatchQueue.global().asyncAfter(deadline: .now() + sec, execute: closure) + DispatchQueue.global().asyncAfter(deadline: .now() + sec, execute: closure) } /// Get text standard output func readStream(file: Int32, stream: UnsafeMutablePointer, block: () -> Void) -> String? { - var result: String? - - // Set pipe - let pipe = Pipe() - let original = dup(file); - setvbuf(stream, nil, _IONBF, 0) - dup2(pipe.fileHandleForWriting.fileDescriptor, file) - - pipe.fileHandleForReading.readabilityHandler = { handle in - if let text = String(data: handle.availableData, encoding: .utf8) { - result = (result != nil) ? (result! + text) : text - } + var result: String? + + // Set pipe + let pipe = Pipe() + let original = dup(file); + setvbuf(stream, nil, _IONBF, 0) + dup2(pipe.fileHandleForWriting.fileDescriptor, file) + + pipe.fileHandleForReading.readabilityHandler = { handle in + if let text = String(data: handle.availableData, encoding: .utf8) { + result = (result != nil) ? (result! + text) : text } - - block() - - delay() - - // Revert - fflush(stream) - dup2(original, file) - close(original) - - // Print - print(result ?? "", terminator: "") - - return result + } + + block() + + delay() + + // Revert + fflush(stream) + dup2(original, file) + close(original) + + // Print + print(result ?? "", terminator: "") + + return result } func read_stdout(_ block: () -> Void) -> String? { - readStream(file: STDOUT_FILENO, stream: stdout, block: block) + readStream(file: STDOUT_FILENO, stream: stdout, block: block) } func read_stderr(_ block: () -> Void) -> String? { - readStream(file: STDERR_FILENO, stream: stderr, block: block) + readStream(file: STDERR_FILENO, stream: stderr, block: block) } @@ -140,1047 +140,1047 @@ let Interval = #"\{average:\#(SECS),duration:\#(SECS)\}"# let Empty = ">$" fileprivate func testAll(_ logger: LogProtocol, categoryTag: String = CategoryTag, metadata: String = "") { - let padding = #"[\|\├\s]+"# - - XCTAssert(logger.log("log")?.match(#"\#(categoryTag)\#(padding)\#(LogTag) \#(Location)\#(metadata) log"#) == true) - - XCTAssert(logger.trace()?.match(#"\#(categoryTag)\#(padding)\#(TraceTag) \#(Location)\#(metadata) \{func:testAll\(_:categoryTag:metadata:\),thread:\{name:main,number:1\}"#) == true) - XCTAssert(logger.trace("start")?.match(#"\#(categoryTag)\#(padding)\#(TraceTag) \#(Location)\#(metadata) \{func:testAll\(_:categoryTag:metadata:\),thread:\{name:main,number:1\}\} start"#) == true) - - XCTAssert(logger.debug("debug")?.match(#"\#(categoryTag)\#(padding)\#(DebugTag) \#(Location)\#(metadata) debug"#) == true) - - XCTAssert(logger.info("info")?.match(#"\#(categoryTag)\#(padding)\#(InfoTag) \#(Location)\#(metadata) info"#) == true) - - XCTAssert(logger.warning("warning")?.match(#"\#(categoryTag)\#(padding)\#(WarningTag) \#(Location)\#(metadata) warning"#) == true) - XCTAssert(logger.error("error")?.match(#"\#(categoryTag)\#(padding)\#(ErrorTag) \#(Location)\#(metadata) error"#) == true) - - XCTAssertNil(logger.assert(true)) - XCTAssertNil(logger.assert(true, "assert")) - XCTAssert(logger.assert(false)?.match(#"\#(categoryTag)\#(padding)\#(AssertTag) \#(Location)\#(metadata)"#) == true) - XCTAssert(logger.assert(false, "assert")?.match(#"\#(categoryTag)\#(padding)\#(AssertTag) \#(Location)\#(metadata) assert"#) == true) - - XCTAssert(logger.fault("fault")?.match(#"\#(categoryTag)\#(padding)\#(FaultTag) \#(Location)\#(metadata) fault"#) == true) - - XCTAssert(read_stdout { logger.scope("scope") { _ in delay() } }?.match(#"\#(categoryTag)\#(padding)└ \[scope\] \(\#(SECS)\)"#) == true) - XCTAssert(read_stdout { logger.interval("signpost") { delay() } }?.match(#"\#(categoryTag)\#(padding)\[INTERVAL\] \#(Location)\#(metadata) \#(Interval) signpost$"#) == true) + let padding = #"[\|\├\s]+"# + + XCTAssert(logger.log("log")?.match(#"\#(categoryTag)\#(padding)\#(LogTag) \#(Location)\#(metadata) log"#) == true) + + XCTAssert(logger.trace()?.match(#"\#(categoryTag)\#(padding)\#(TraceTag) \#(Location)\#(metadata) \{func:testAll\(_:categoryTag:metadata:\),thread:\{name:main,number:1\}"#) == true) + XCTAssert(logger.trace("start")?.match(#"\#(categoryTag)\#(padding)\#(TraceTag) \#(Location)\#(metadata) \{func:testAll\(_:categoryTag:metadata:\),thread:\{name:main,number:1\}\} start"#) == true) + + XCTAssert(logger.debug("debug")?.match(#"\#(categoryTag)\#(padding)\#(DebugTag) \#(Location)\#(metadata) debug"#) == true) + + XCTAssert(logger.info("info")?.match(#"\#(categoryTag)\#(padding)\#(InfoTag) \#(Location)\#(metadata) info"#) == true) + + XCTAssert(logger.warning("warning")?.match(#"\#(categoryTag)\#(padding)\#(WarningTag) \#(Location)\#(metadata) warning"#) == true) + XCTAssert(logger.error("error")?.match(#"\#(categoryTag)\#(padding)\#(ErrorTag) \#(Location)\#(metadata) error"#) == true) + + XCTAssertNil(logger.assert(true)) + XCTAssertNil(logger.assert(true, "assert")) + XCTAssert(logger.assert(false)?.match(#"\#(categoryTag)\#(padding)\#(AssertTag) \#(Location)\#(metadata)"#) == true) + XCTAssert(logger.assert(false, "assert")?.match(#"\#(categoryTag)\#(padding)\#(AssertTag) \#(Location)\#(metadata) assert"#) == true) + + XCTAssert(logger.fault("fault")?.match(#"\#(categoryTag)\#(padding)\#(FaultTag) \#(Location)\#(metadata) fault"#) == true) + + XCTAssert(read_stdout { logger.scope("scope") { _ in delay() } }?.match(#"\#(categoryTag)\#(padding)└ \[scope\] \(\#(SECS)\)"#) == true) + XCTAssert(read_stdout { logger.interval("signpost") { delay() } }?.match(#"\#(categoryTag)\#(padding)\[INTERVAL\] \#(Location)\#(metadata) \#(Interval) signpost$"#) == true) } final class DLogTests: XCTestCase { - - // MARK: Tests - - - func test_Log() { - let logger = DLog() - testAll(logger) - } - - // MARK: - Category - - func test_Category() { - let logger = DLog() - let netLogger = logger["NET"] - - testAll(netLogger, categoryTag: #"\[NET\]"#) - } - - // MARK: - Text - - func test_textEmoji() { - let logger = DLog(.textEmoji => .stdout) - - XCTAssert(logger.log("log")?.match(#"\#(CategoryTag) 💬 \#(LogTag) \#(Location) log"#) == true) - - XCTAssert(logger.trace()?.match(#"\#(CategoryTag) #️⃣ \#(TraceTag) \#(Location) \{func:\#(#function)"#) == true) - XCTAssert(logger.debug("debug")?.match(#"\#(CategoryTag) ▶️ \#(DebugTag) \#(Location) debug"#) == true) - - XCTAssert(logger.info("info")?.match(#"\#(CategoryTag) ✅ \#(InfoTag) \#(Location) info"#) == true) - - XCTAssert(logger.warning("warning")?.match(#"\#(CategoryTag) ⚠️ \#(WarningTag) \#(Location) warning"#) == true) - XCTAssert(logger.error("error")?.match(#"\#(CategoryTag) ⚠️ \#(ErrorTag) \#(Location) error"#) == true) - - XCTAssertNil(logger.assert(true)) - XCTAssert(logger.assert(false)?.match(#"\#(CategoryTag) 🅰️ \#(AssertTag) \#(Location)"#) == true) - XCTAssert(logger.fault("fault")?.match(#"\#(CategoryTag) 🆘 \#(FaultTag) \#(Location) fault"#) == true) - - XCTAssert(read_stdout { logger.scope("My Scope") { _ in } }?.match(#"\[My Scope\]"#) == true) - XCTAssert(read_stdout { logger.interval("My Interval") {} }?.match(#"🕒 \[INTERVAL\]"#) == true) - } - - func test_textColored() { - var config = LogConfig() - config.options = .all - let logger = DLog(.textColored => .stdout, config: config) - - let reset = "\u{001b}[0m" - XCTAssert(logger.trace()?.contains(reset) == true) - XCTAssert(logger.info("info")?.contains(reset) == true) - XCTAssert(logger.debug("debug")?.contains(reset) == true) - XCTAssert(logger.error("error")?.contains(reset) == true) - XCTAssert(logger.assert(false, "assert")?.contains(reset) == true) - XCTAssert(logger.fault("fault")?.contains(reset) == true) - - XCTAssert(read_stdout { logger.scope("scope") { _ in } }?.contains(reset) == true) - XCTAssert(read_stdout { logger.interval("interval") {} }?.contains(reset) == true) - } - - // MARK: - Standard - - func test_stdout_err() { - let logOut = DLog(.stdout) - XCTAssert(read_stdout { logOut.trace() }?.match(#"\#(CategoryTag) \#(TraceTag) \#(Location) \{func:\#(#function)"#) == true) - - let logErr = DLog(.stderr) - XCTAssert(read_stderr { logErr.trace() }?.match(#"\#(CategoryTag) \#(TraceTag) \#(Location) \{func:\#(#function)"#) == true) - } - - // MARK: - File - - func test_File() { - let filePath = "dlog.txt" - - do { - // Recreate file - let logger = DLog(.textPlain => .file(filePath, append: false)) - logger.trace() - delay(0.1) - var text = try String(contentsOfFile: filePath) - XCTAssert(text.match(#"\#(CategoryTag) \#(TraceTag) \#(Location) \{func:\#(#function)"#)) - - // Append - let logger2 = DLog(.textPlain => .file(filePath, append: true)) - logger2.debug("debug") - delay(0.1) - text = try String(contentsOfFile: filePath) - XCTAssert(text.match(#"\#(CategoryTag) \#(TraceTag) \#(Location) \{func:\#(#function)"#)) - XCTAssert(text.match(#"\#(CategoryTag) \#(DebugTag) \#(Location) debug$"#)) - } - catch { - XCTFail(error.localizedDescription) - } - } - - // MARK: - OSLog - - func test_oslog() { - let logger = DLog(.oslog) - XCTAssertNotNil(logger.debug("debug")) - logger.interval("signpost") { - logger.debug("signpost") - } - - - let log2 = DLog(.oslog("com.dlog.test")) - XCTAssertNotNil(log2.debug("debug")) - } - - // MARK: - Net - - func test_net() { - let logger = DLog(.net) - XCTAssertNotNil(logger.debug("oslog")) - - logger.scope("hello") { scope in - scope.log("log") - scope.debug("debug") - scope.trace() - scope.warning("warning") - scope.error("error") - scope.assert(false, "assert") - scope.fault("fatal") - scope.interval("interval") { - delay() - } - } - -// wait { exp in -// } - - let log2 = DLog(.net("MyName")) - XCTAssertNotNil(log2.debug("oslog")) - } - - // MARK: - Filter - - func test_Filter() { - // Time - let timeLogger = DLog(.textPlain => .filter(item: { $0.time < Date() }) => .stdout) - XCTAssertNotNil(timeLogger.info("info")) - - // Category - let categoryLogger = DLog(.textPlain => .filter { $0.category == "NET" } => .stdout) - XCTAssertNil(categoryLogger.info("info")) - let netLogger = categoryLogger["NET"] - XCTAssertNotNil(netLogger.info("info")) - - // Type - let typeLogger = DLog(.textPlain => .filter { $0.type == .debug } => .stdout) - XCTAssertNil(typeLogger.trace()) - XCTAssertNil(typeLogger.info("info")) - XCTAssertNotNil(typeLogger.debug("debug")) - - // File name - let fileLogger = DLog(.textPlain => .filter { $0.fileName == "DLogTests.swift" } => .stdout) - XCTAssertNotNil(fileLogger.info("info")) - - // Func name - let funcLogger = DLog(.textPlain => .filter { $0.funcName == "test_Filter()" } => .stdout) - XCTAssertNotNil(funcLogger.info("info")) - - // Line - let lineLogger = DLog(.textPlain => .filter { $0.line > #line } => .stdout) - XCTAssertNotNil(lineLogger.info("info")) - - // Text - let textLogger = DLog(.textPlain => .filter { $0.text.contains("hello") } => .stdout) - XCTAssertNotNil(textLogger.info("hello world")) - XCTAssertNotNil(textLogger.debug("hello")) - XCTAssertNil(textLogger.info("info")) - XCTAssertNil(read_stdout { textLogger.interval("interval") { delay(0.3) } }) - XCTAssertNotNil(read_stdout { textLogger.interval("hello interval") { Thread.sleep(forTimeInterval: 0.3) } }) - - // Scope - let scopeLogger = DLog(.textPlain => .filter { $0.name == "scope" } => .stdout) - XCTAssertNil(read_stdout { scopeLogger.scope("load") { _ in } }) - XCTAssertNotNil(read_stdout { scopeLogger.scope("load") { $0.log("load") } }) - XCTAssertNotNil(read_stdout { scopeLogger.scope("scope") { $0.log("scope") } }) - - // Item & Scope - let filter = Filter(isItem: { $0.scope?.name == "Load" }, isScope: { $0.name == "Load" }) - let itemScopeLogger = DLog(.textPlain => filter => .stdout) - XCTAssertNil(itemScopeLogger.info("info")) - XCTAssertNotNil(read_stdout { - itemScopeLogger.scope("Load") { scope in - XCTAssertNotNil(scope.debug("load")) - XCTAssertNotNil(scope.error("load")) - XCTAssertNil(read_stdout { - scope.scope("Parse") { scope in - XCTAssertNil(scope.debug("parse")) - XCTAssertNil(scope.error("parse")) - } - }) - } - }) - XCTAssertNil(itemScopeLogger.fault("fault")) - - // Metadata - let metadataLogger = DLog(.textPlain - => .filter { (item: LogItem) in item.metadata["id"] as? Int == 12345 } - => .stdout) - metadataLogger.metadata["id"] = 12 - XCTAssertNil(metadataLogger.log("load")) - metadataLogger.metadata["id"] = 12345 - XCTAssertNotNil(metadataLogger.log("load")) - metadataLogger.metadata.clear() - XCTAssertNil(metadataLogger.log("load")) - } - - // MARK: - Disabled - - func test_Disabled() { - - let failBool: () -> Bool = { - XCTFail() - return false - } - - let failMessage: () -> LogMessage = { - XCTFail() - return "" - } - - let test: (LogProtocol, XCTestExpectation) -> Void = { logger, expectation in - logger.log(failMessage()) - logger.trace(failMessage()) - logger.debug("\(failMessage())") - logger.info(failMessage()) - logger.warning(failMessage()) - logger.error(failMessage()) - logger.fault(failMessage()) - logger.assert(failBool(), failMessage()) - logger.scope("scope") { _ in expectation.fulfill() } - logger.interval("interval") { expectation.fulfill() } - } - - let logger = DLog.disabled - let scope = logger.scope("scope") - let netLogger = logger["NET"] - - wait { expectation in - expectation.expectedFulfillmentCount = 6 - - XCTAssertNil( - read_stdout { - test(logger, expectation) - test(netLogger, expectation) - test(scope, expectation) - } - ) - } - } - - // MARK: - Thread safe - // categories, scopes, interavls - - func test_NonBlock() { - let logger = DLog(.textPlain - => .stdout - => .file("dlog.txt") - => .oslog - => .filter { $0.type == .debug } - => .net) - - let netLogger = logger["NET"] - netLogger.log("log") - netLogger.trace() - netLogger.debug("debug") - netLogger.info("info") - netLogger.warning("warning") - netLogger.error("error") - netLogger.assert(false) - netLogger.fault("fault") - netLogger.scope("scope") { _ in } - netLogger.interval("signpost") { } - - let scope = logger.scope("test") { scope in - scope.log("log") - scope.trace() - scope.debug("debug") - scope.info("info") - scope.warning("warning") - scope.error("error") - scope.assert(false) - scope.fault("fault") - scope.scope("scope") { _ in } - scope.interval("signpost") { } - } - - XCTAssert(scope.duration < 0.2) - } - - // MARK: - Config - - func test_ConfigEmpty() { - var config = LogConfig() - config.options = [] - - let logger = DLog(config: config) - - XCTAssert(logger.trace()?.match(#"\{func:test_ConfigEmpty\(\),thread:\{name:main,number:1\}\}$"#) == true) - } - - func test_ConfigAll() { - var config = LogConfig() - config.options = .all - - let logger = DLog(config: config) - - XCTAssert(logger.trace()?.match(#"\#(Sign) \#(Time) \#(Level) \#(CategoryTag) \#(TraceTag) \#(Location) \{func:test_ConfigAll\(\),thread:\{name:main,number:1\}\}$"#) == true) - } - - func test_ConfigCategory() { - let logger = DLog() - - let viewLogger = logger["VIEW"] - - var config = LogConfig() - config.sign = ">" - config.options = [.sign, .time, .category, .type, .level] - config.traceConfig.options = .queue - config.intervalConfig.options = .total - - let netLogger = logger.category(name: "NET", config: config) - - // Trace - XCTAssert(logger.trace()?.match(#"\#(Sign) \#(Time) \#(CategoryTag) \#(TraceTag) \#(Location) \{func:test_ConfigCategory\(\),thread:\{name:main,number:1\}\}$"#) == true) - XCTAssert(viewLogger.trace()?.match(#"\#(Sign) \#(Time) \[VIEW\] \#(TraceTag) \#(Location) \{func:test_ConfigCategory\(\),thread:\{name:main,number:1\}\}$"#) == true) - XCTAssert(netLogger.trace()?.match(#"> \#(Time) \#(Level) \[NET\] \#(TraceTag) \{queue:com.apple.main-thread\}$"#) == true) - - // Interval - XCTAssert(read_stdout { logger.interval("signpost") { delay() }}?.match(#"\#(Sign) \#(Time) \#(CategoryTag) \#(IntervalTag) \#(Location) \#(Interval) signpost$"#) == true) - XCTAssert(read_stdout { viewLogger.interval("signpost") { delay() }}?.match(#"\#(Sign) \#(Time) \[VIEW\] \#(IntervalTag) \#(Location) \#(Interval) signpost$"#) == true) - XCTAssert(read_stdout { netLogger.interval("signpost") { delay() }}?.match(#"> \#(Time) \#(Level) \[NET\] \#(IntervalTag) \{total:\#(SECS)\} signpost$"#) == true) + + // MARK: Tests - + + func test_Log() { + let logger = DLog() + testAll(logger) + } + + // MARK: - Category + + func test_Category() { + let logger = DLog() + let netLogger = logger["NET"] + + testAll(netLogger, categoryTag: #"\[NET\]"#) + } + + // MARK: - Text + + func test_textEmoji() { + let logger = DLog(.textEmoji => .stdout) + + XCTAssert(logger.log("log")?.match(#"\#(CategoryTag) 💬 \#(LogTag) \#(Location) log"#) == true) + + XCTAssert(logger.trace()?.match(#"\#(CategoryTag) #️⃣ \#(TraceTag) \#(Location) \{func:\#(#function)"#) == true) + XCTAssert(logger.debug("debug")?.match(#"\#(CategoryTag) ▶️ \#(DebugTag) \#(Location) debug"#) == true) + + XCTAssert(logger.info("info")?.match(#"\#(CategoryTag) ✅ \#(InfoTag) \#(Location) info"#) == true) + + XCTAssert(logger.warning("warning")?.match(#"\#(CategoryTag) ⚠️ \#(WarningTag) \#(Location) warning"#) == true) + XCTAssert(logger.error("error")?.match(#"\#(CategoryTag) ⚠️ \#(ErrorTag) \#(Location) error"#) == true) + + XCTAssertNil(logger.assert(true)) + XCTAssert(logger.assert(false)?.match(#"\#(CategoryTag) 🅰️ \#(AssertTag) \#(Location)"#) == true) + XCTAssert(logger.fault("fault")?.match(#"\#(CategoryTag) 🆘 \#(FaultTag) \#(Location) fault"#) == true) + + XCTAssert(read_stdout { logger.scope("My Scope") { _ in } }?.match(#"\[My Scope\]"#) == true) + XCTAssert(read_stdout { logger.interval("My Interval") {} }?.match(#"🕒 \[INTERVAL\]"#) == true) + } + + func test_textColored() { + var config = LogConfig() + config.options = .all + let logger = DLog(.textColored => .stdout, config: config) + + let reset = "\u{001b}[0m" + XCTAssert(logger.trace()?.contains(reset) == true) + XCTAssert(logger.info("info")?.contains(reset) == true) + XCTAssert(logger.debug("debug")?.contains(reset) == true) + XCTAssert(logger.error("error")?.contains(reset) == true) + XCTAssert(logger.assert(false, "assert")?.contains(reset) == true) + XCTAssert(logger.fault("fault")?.contains(reset) == true) + + XCTAssert(read_stdout { logger.scope("scope") { _ in } }?.contains(reset) == true) + XCTAssert(read_stdout { logger.interval("interval") {} }?.contains(reset) == true) + } + + // MARK: - Standard + + func test_stdout_err() { + let logOut = DLog(.stdout) + XCTAssert(read_stdout { logOut.trace() }?.match(#"\#(CategoryTag) \#(TraceTag) \#(Location) \{func:\#(#function)"#) == true) + + let logErr = DLog(.stderr) + XCTAssert(read_stderr { logErr.trace() }?.match(#"\#(CategoryTag) \#(TraceTag) \#(Location) \{func:\#(#function)"#) == true) + } + + // MARK: - File + + func test_File() { + let filePath = "dlog.txt" + + do { + // Recreate file + let logger = DLog(.textPlain => .file(filePath, append: false)) + logger.trace() + delay(0.1) + var text = try String(contentsOfFile: filePath) + XCTAssert(text.match(#"\#(CategoryTag) \#(TraceTag) \#(Location) \{func:\#(#function)"#)) + + // Append + let logger2 = DLog(.textPlain => .file(filePath, append: true)) + logger2.debug("debug") + delay(0.1) + text = try String(contentsOfFile: filePath) + XCTAssert(text.match(#"\#(CategoryTag) \#(TraceTag) \#(Location) \{func:\#(#function)"#)) + XCTAssert(text.match(#"\#(CategoryTag) \#(DebugTag) \#(Location) debug$"#)) + } + catch { + XCTFail(error.localizedDescription) + } + } + + // MARK: - OSLog + + func test_oslog() { + let logger = DLog(.oslog) + XCTAssertNotNil(logger.debug("debug")) + logger.interval("signpost") { + logger.debug("signpost") } -} - -final class MetadataTests: XCTestCase { - let idText = #"\(id:12345\)"# - let nameText = #"\(name:Bob\)"# - let idNameText = #"\(id:12345,name:Bob\)"# - func test_metadata() { - let logger = DLog() - logger.metadata["id"] = 12345 - testAll(logger, metadata: #" \#(idText)"#) - - logger.metadata["id"] = nil - testAll(logger) - - logger.metadata["id"] = 12345 - logger.metadata["name"] = "Bob" - testAll(logger, metadata: #" \#(idNameText)"#) - - logger.metadata.clear() - testAll(logger) + let log2 = DLog(.oslog("com.dlog.test")) + XCTAssertNotNil(log2.debug("debug")) + } + + // MARK: - Net + + func test_net() { + let logger = DLog(.net) + XCTAssertNotNil(logger.debug("oslog")) + + logger.scope("hello") { scope in + scope.log("log") + scope.debug("debug") + scope.trace() + scope.warning("warning") + scope.error("error") + scope.assert(false, "assert") + scope.fault("fatal") + scope.interval("interval") { + delay() + } } - func test_metadata_category() { - let logger = DLog(metadata: ["id" : 12345]) - XCTAssert(logger.metadata["id"] as? Int == 12345) - XCTAssert(logger.log("log")?.match(idText) == true) - - // Category with inherited metadata - let net = logger["NET"] - XCTAssert(net.log("log")?.match(idText) == true) - net.metadata["name"] = "Bob" - XCTAssert(net.log("log")?.match(idNameText) == true) - net.metadata.clear() - XCTAssert(net.log("log")?.match(idNameText) == false) - - XCTAssert(logger.log("log")?.match(idText) == true) - - // Category with own metadata - let ui = logger.category(name: "UI", metadata: ["name" : "Bob"]) - XCTAssert(ui.log("log")?.match(nameText) == true) - ui.metadata.clear() - XCTAssert(ui.log("log")?.match(idText) == false) - - XCTAssert(logger.log("log")?.match(idText) == true) + // wait { exp in + // } + + let log2 = DLog(.net("MyName")) + XCTAssertNotNil(log2.debug("oslog")) + } + + // MARK: - Filter + + func test_Filter() { + // Time + let timeLogger = DLog(.textPlain => .filter(item: { $0.time < Date() }) => .stdout) + XCTAssertNotNil(timeLogger.info("info")) + + // Category + let categoryLogger = DLog(.textPlain => .filter { $0.category == "NET" } => .stdout) + XCTAssertNil(categoryLogger.info("info")) + let netLogger = categoryLogger["NET"] + XCTAssertNotNil(netLogger.info("info")) + + // Type + let typeLogger = DLog(.textPlain => .filter { $0.type == .debug } => .stdout) + XCTAssertNil(typeLogger.trace()) + XCTAssertNil(typeLogger.info("info")) + XCTAssertNotNil(typeLogger.debug("debug")) + + // File name + let fileLogger = DLog(.textPlain => .filter { $0.fileName == "DLogTests.swift" } => .stdout) + XCTAssertNotNil(fileLogger.info("info")) + + // Func name + let funcLogger = DLog(.textPlain => .filter { $0.funcName == "test_Filter()" } => .stdout) + XCTAssertNotNil(funcLogger.info("info")) + + // Line + let lineLogger = DLog(.textPlain => .filter { $0.line > #line } => .stdout) + XCTAssertNotNil(lineLogger.info("info")) + + // Text + let textLogger = DLog(.textPlain => .filter { $0.text.contains("hello") } => .stdout) + XCTAssertNotNil(textLogger.info("hello world")) + XCTAssertNotNil(textLogger.debug("hello")) + XCTAssertNil(textLogger.info("info")) + XCTAssertNil(read_stdout { textLogger.interval("interval") { delay(0.3) } }) + XCTAssertNotNil(read_stdout { textLogger.interval("hello interval") { Thread.sleep(forTimeInterval: 0.3) } }) + + // Scope + let scopeLogger = DLog(.textPlain => .filter { $0.name == "scope" } => .stdout) + XCTAssertNil(read_stdout { scopeLogger.scope("load") { _ in } }) + XCTAssertNotNil(read_stdout { scopeLogger.scope("load") { $0.log("load") } }) + XCTAssertNotNil(read_stdout { scopeLogger.scope("scope") { $0.log("scope") } }) + + // Item & Scope + let filter = Filter(isItem: { $0.scope?.name == "Load" }, isScope: { $0.name == "Load" }) + let itemScopeLogger = DLog(.textPlain => filter => .stdout) + XCTAssertNil(itemScopeLogger.info("info")) + XCTAssertNotNil(read_stdout { + itemScopeLogger.scope("Load") { scope in + XCTAssertNotNil(scope.debug("load")) + XCTAssertNotNil(scope.error("load")) + XCTAssertNil(read_stdout { + scope.scope("Parse") { scope in + XCTAssertNil(scope.debug("parse")) + XCTAssertNil(scope.error("parse")) + } + }) + } + }) + XCTAssertNil(itemScopeLogger.fault("fault")) + + // Metadata + let metadataLogger = DLog(.textPlain + => .filter { (item: LogItem) in item.metadata["id"] as? Int == 12345 } + => .stdout) + metadataLogger.metadata["id"] = 12 + XCTAssertNil(metadataLogger.log("load")) + metadataLogger.metadata["id"] = 12345 + XCTAssertNotNil(metadataLogger.log("load")) + metadataLogger.metadata.clear() + XCTAssertNil(metadataLogger.log("load")) + } + + // MARK: - Disabled + + func test_Disabled() { + + let failBool: () -> Bool = { + XCTFail() + return false } - func test_metadata_scope() { - let logger = DLog() - logger.metadata["id"] = 12345 - XCTAssert(logger.log("log")?.match(idText) == true) - - // Scope with inherited metadata - logger.scope("scope") { scope in - XCTAssert(scope.log("log")?.match(self.idText) == true) - scope.metadata["name"] = "Bob" - XCTAssert(scope.log("log")?.match(self.idNameText) == true) - scope.metadata.clear() - XCTAssert(scope.log("log")?.match(self.idNameText) == false) - } - - XCTAssert(logger.log("log")?.match(idText) == true) - - // Scope with own metadata - logger.scope("scope", metadata: ["name" : "Bob"]) { scope in - XCTAssert(scope.log("log")?.match(self.nameText) == true) - scope.metadata.clear() - XCTAssert(scope.log("log")?.match(self.nameText) == false) + let failMessage: () -> LogMessage = { + XCTFail() + return "" + } + + let test: (LogProtocol, XCTestExpectation) -> Void = { logger, expectation in + logger.log(failMessage()) + logger.trace(failMessage()) + logger.debug("\(failMessage())") + logger.info(failMessage()) + logger.warning(failMessage()) + logger.error(failMessage()) + logger.fault(failMessage()) + logger.assert(failBool(), failMessage()) + logger.scope("scope") { _ in expectation.fulfill() } + logger.interval("interval") { expectation.fulfill() } + } + + let logger = DLog.disabled + let scope = logger.scope("scope") + let netLogger = logger["NET"] + + wait { expectation in + expectation.expectedFulfillmentCount = 6 + + XCTAssertNil( + read_stdout { + test(logger, expectation) + test(netLogger, expectation) + test(scope, expectation) } - - XCTAssert(logger.log("log")?.match(idText) == true) + ) } + } + + // MARK: - Thread safe + // categories, scopes, interavls + + func test_NonBlock() { + let logger = DLog(.textPlain + => .stdout + => .file("dlog.txt") + => .oslog + => .filter { $0.type == .debug } + => .net) - func test_metadata_config() { - var config = LogConfig() - config.options = [.compact] - let logger = DLog(config: config) - - logger.metadata["id"] = 12345 - XCTAssert(logger.log("log")?.match(idText) == false) + let netLogger = logger["NET"] + netLogger.log("log") + netLogger.trace() + netLogger.debug("debug") + netLogger.info("info") + netLogger.warning("warning") + netLogger.error("error") + netLogger.assert(false) + netLogger.fault("fault") + netLogger.scope("scope") { _ in } + netLogger.interval("signpost") { } + + let scope = logger.scope("test") { scope in + scope.log("log") + scope.trace() + scope.debug("debug") + scope.info("info") + scope.warning("warning") + scope.error("error") + scope.assert(false) + scope.fault("fault") + scope.scope("scope") { _ in } + scope.interval("signpost") { } } + + XCTAssert(scope.duration < 0.2) + } + + // MARK: - Config + + func test_ConfigEmpty() { + var config = LogConfig() + config.options = [] + + let logger = DLog(config: config) + + XCTAssert(logger.trace()?.match(#"\{func:test_ConfigEmpty\(\),thread:\{name:main,number:1\}\}$"#) == true) + } + + func test_ConfigAll() { + var config = LogConfig() + config.options = .all + + let logger = DLog(config: config) + + XCTAssert(logger.trace()?.match(#"\#(Sign) \#(Time) \#(Level) \#(CategoryTag) \#(TraceTag) \#(Location) \{func:test_ConfigAll\(\),thread:\{name:main,number:1\}\}$"#) == true) + } + + func test_ConfigCategory() { + let logger = DLog() + + let viewLogger = logger["VIEW"] + + var config = LogConfig() + config.sign = ">" + config.options = [.sign, .time, .category, .type, .level] + config.traceConfig.options = .queue + config.intervalConfig.options = .total + + let netLogger = logger.category(name: "NET", config: config) + + // Trace + XCTAssert(logger.trace()?.match(#"\#(Sign) \#(Time) \#(CategoryTag) \#(TraceTag) \#(Location) \{func:test_ConfigCategory\(\),thread:\{name:main,number:1\}\}$"#) == true) + XCTAssert(viewLogger.trace()?.match(#"\#(Sign) \#(Time) \[VIEW\] \#(TraceTag) \#(Location) \{func:test_ConfigCategory\(\),thread:\{name:main,number:1\}\}$"#) == true) + XCTAssert(netLogger.trace()?.match(#"> \#(Time) \#(Level) \[NET\] \#(TraceTag) \{queue:com.apple.main-thread\}$"#) == true) + + // Interval + XCTAssert(read_stdout { logger.interval("signpost") { delay() }}?.match(#"\#(Sign) \#(Time) \#(CategoryTag) \#(IntervalTag) \#(Location) \#(Interval) signpost$"#) == true) + XCTAssert(read_stdout { viewLogger.interval("signpost") { delay() }}?.match(#"\#(Sign) \#(Time) \[VIEW\] \#(IntervalTag) \#(Location) \#(Interval) signpost$"#) == true) + XCTAssert(read_stdout { netLogger.interval("signpost") { delay() }}?.match(#"> \#(Time) \#(Level) \[NET\] \#(IntervalTag) \{total:\#(SECS)\} signpost$"#) == true) + } } -final class FormatTests: XCTestCase { +final class MetadataTests: XCTestCase { + + let idText = #"\(id:12345\)"# + let nameText = #"\(name:Bob\)"# + let idNameText = #"\(id:12345,name:Bob\)"# + + func test_metadata() { + let logger = DLog() + logger.metadata["id"] = 12345 + testAll(logger, metadata: #" \#(idText)"#) - func test_literals() { - let logger = DLog() - - XCTAssert(logger.log(1)?.match("1$") == true) // int - XCTAssert(logger.log(2.0)?.match("2.0$") == true) // float - XCTAssert(logger.log(true)?.match("true$") == true) // bool - XCTAssert(logger.log("text")?.match("text$") == true) // string - XCTAssert(logger.log([1, "2", 3.0])?.match("\\[1, \"2\", 3.0\\]$") == true) // array - XCTAssert(logger.log([1 : 1, "2" : "2", 3 : 3.0])?.match("\\[\\(1, 1\\), \\(\"2\", \"2\"\\), \\(3, 3.0\\)\\]$") == true) // dictionary - } - - func test_logMessage() { - let logger = DLog() - - // Any - let text: Any = "some text" - XCTAssert(logger.debug("\(text)")?.match("some text") == true) - - // Error - let error = NSError(domain: "domain", code: 100, userInfo: [NSLocalizedDescriptionKey : "error"]) - XCTAssert(logger.error("\(error)")?.match("Error Domain=domain Code=100 \"error\" UserInfo=\\{NSLocalizedDescription=error\\}") == true) - XCTAssert(logger.error("\(error as Error)")?.match("Error Domain=domain Code=100 \"error\" UserInfo=\\{NSLocalizedDescription=error\\}") == true) - XCTAssert(logger.error("\(error.localizedDescription)")?.match("error") == true) - - // Enum - enum MyEnum { case one, two } - let myEnum = MyEnum.one - XCTAssert(logger.error("\(myEnum)")?.match("one") == true) - XCTAssert(logger.error("\(MyEnum.one)")?.match("one") == true) - - // OptionSet - XCTAssert(logger.error("\(NSCalendar.Options.matchLast)")?.match("NSCalendarOptions\\(rawValue: 8192\\)") == true) - - // Notification - let notification = Notification.Name.NSCalendarDayChanged - XCTAssert(logger.debug("\(notification.rawValue)")?.match("NSCalendarDayChanged") == true) - - // Array - let array = [1, 2, 3] - XCTAssert(logger.debug("\(array)")?.match("\\[1, 2, 3\\]") == true) + logger.metadata["id"] = nil + testAll(logger) + + logger.metadata["id"] = 12345 + logger.metadata["name"] = "Bob" + testAll(logger, metadata: #" \#(idNameText)"#) + + logger.metadata.clear() + testAll(logger) + } + + func test_metadata_category() { + let logger = DLog(metadata: ["id" : 12345]) + XCTAssert(logger.metadata["id"] as? Int == 12345) + XCTAssert(logger.log("log")?.match(idText) == true) + + // Category with inherited metadata + let net = logger["NET"] + XCTAssert(net.log("log")?.match(idText) == true) + net.metadata["name"] = "Bob" + XCTAssert(net.log("log")?.match(idNameText) == true) + net.metadata.clear() + XCTAssert(net.log("log")?.match(idNameText) == false) + + XCTAssert(logger.log("log")?.match(idText) == true) + + // Category with own metadata + let ui = logger.category(name: "UI", metadata: ["name" : "Bob"]) + XCTAssert(ui.log("log")?.match(nameText) == true) + ui.metadata.clear() + XCTAssert(ui.log("log")?.match(idText) == false) + + XCTAssert(logger.log("log")?.match(idText) == true) + } + + func test_metadata_scope() { + let logger = DLog() + logger.metadata["id"] = 12345 + XCTAssert(logger.log("log")?.match(idText) == true) + + // Scope with inherited metadata + logger.scope("scope") { scope in + XCTAssert(scope.log("log")?.match(self.idText) == true) + scope.metadata["name"] = "Bob" + XCTAssert(scope.log("log")?.match(self.idNameText) == true) + scope.metadata.clear() + XCTAssert(scope.log("log")?.match(self.idNameText) == false) } - func test_Privacy() { - let logger = DLog() - - let empty = "" - let cardNumber = "1234 5678 9012 3456" - let greeting = "Hello World!" - let number = 1234567890 - - XCTAssert(logger.log("Default: \(cardNumber)")?.match(cardNumber) == true) - XCTAssert(logger.log("Public: \(cardNumber, privacy: .public)")?.match(cardNumber) == true) - - XCTAssert(logger.log("Private: \(cardNumber, privacy: .private)")?.match("") == true) - - XCTAssert(logger.log("Private hash: \(cardNumber, privacy: .private(mask: .hash))")?.match("[0-9a-fA-F]+") == true) - XCTAssert(logger.log("Private hash: \(cardNumber, privacy: .private(mask: .hash, auto: false))")?.match("[0-9a-fA-F]+") == true) - - XCTAssert(logger.log("Private random: \(empty, privacy: .private(mask: .random))")?.match(": $") == true) - XCTAssert(logger.log("Private random: \(cardNumber, privacy: .private(mask: .random))")?.match(cardNumber) == false) - XCTAssert(logger.log("Private random: \(greeting, privacy: .private(mask: .random))")?.match(greeting) == false) - - XCTAssert(logger.log("Private redact: \(empty, privacy: .private(mask: .redact))")?.match(": $") == true) - XCTAssert(logger.log("Private redact: \(cardNumber, privacy: .private(mask: .redact))")?.match("0000 0000 0000 0000") == true) - XCTAssert(logger.log("Private redact: \(greeting, privacy: .private(mask: .redact))")?.match("XXXXX XXXXX!") == true) - - XCTAssert(logger.log("Private shuffle: \("1 2 3", privacy: .private(mask: .shuffle))")?.match("1 2 3") == true) - XCTAssert(logger.log("Private shuffle: \(cardNumber, privacy: .private(mask: .shuffle))")?.match(cardNumber) == false) - XCTAssert(logger.log("Private shuffle: \(greeting, privacy: .private(mask: .shuffle))")?.match(greeting) == false) - - XCTAssert(logger.log("Private custom: \(cardNumber, privacy: .private(mask: .custom(value: "")))")?.match("") == true) - - XCTAssert(logger.log("Private partial: \(empty, privacy: .private(mask: .partial(first: -1, last: -2)))")?.match(": $") == true) - XCTAssert(logger.log("Private partial: \(cardNumber, privacy: .private(mask: .partial(first: -1, last: -2)))")?.match("[\\*]{19}") == true) - XCTAssert(logger.log("Private partial: \(cardNumber, privacy: .private(mask: .partial(first: 0, last: 0)))")?.match("[\\*]{19}") == true) - XCTAssert(logger.log("Private partial: \(cardNumber, privacy: .private(mask: .partial(first: 1, last: 4)))")?.match("1[\\*]{14}3456") == true) - XCTAssert(logger.log("Private partial: \(cardNumber, privacy: .private(mask: .partial(first: 10, last: 10)))")?.match(cardNumber) == true) - - XCTAssert(logger.log("Private reduce: \(cardNumber, privacy: .private(mask: .reduce(length: -3)))")?.match("...") == true) - XCTAssert(logger.log("Private reduce: \(cardNumber, privacy: .private(mask: .reduce(length: 0)))")?.match("...") == true) - XCTAssert(logger.log("Private reduce: \(cardNumber, privacy: .private(mask: .reduce(length: 1)))")?.match("...6") == true) - XCTAssert(logger.log("Private reduce: \(cardNumber, privacy: .private(mask: .reduce(length: 2)))")?.match("1...6") == true) - XCTAssert(logger.log("Private reduce: \(cardNumber, privacy: .private(mask: .reduce(length: 7)))")?.match("123...3456") == true) - XCTAssert(logger.log("Private reduce: \(cardNumber, privacy: .private(mask: .reduce(length: 100)))")?.match(cardNumber) == true) - XCTAssert(logger.log("Private reduce: \(greeting, privacy: .private(mask: .reduce(length: 6)))")?.match("Hel...ld!") == true) - - XCTAssert(logger.log("Private reduce: \(number, privacy: .private(mask: .reduce(length: 6)))")?.match("123...890") == true) + XCTAssert(logger.log("log")?.match(idText) == true) + + // Scope with own metadata + logger.scope("scope", metadata: ["name" : "Bob"]) { scope in + XCTAssert(scope.log("log")?.match(self.nameText) == true) + scope.metadata.clear() + XCTAssert(scope.log("log")?.match(self.nameText) == false) } - func test_DateFormat() { - let logger = DLog() - - let date = Date(timeIntervalSince1970: 1645026131) // 2022-02-16 15:42:11 +0000 - - // Default - XCTAssert(logger.log("\(date)")?.match("2022-02-16 15:42:11 \\+0000") == true) + XCTAssert(logger.log("log")?.match(idText) == true) + } + + func test_metadata_config() { + var config = LogConfig() + config.options = [.compact] + let logger = DLog(config: config) + + logger.metadata["id"] = 12345 + XCTAssert(logger.log("log")?.match(idText) == false) + } +} - // Date only +final class FormatTests: XCTestCase { + + func test_literals() { + let logger = DLog() + + XCTAssert(logger.log(1)?.match("1$") == true) // int + XCTAssert(logger.log(2.0)?.match("2.0$") == true) // float + XCTAssert(logger.log(true)?.match("true$") == true) // bool + XCTAssert(logger.log("text")?.match("text$") == true) // string + XCTAssert(logger.log([1, "2", 3.0])?.match("\\[1, \"2\", 3.0\\]$") == true) // array + XCTAssert(logger.log([1 : 1, "2" : "2", 3 : 3.0])?.match("\\[\\(1, 1\\), \\(\"2\", \"2\"\\), \\(3, 3.0\\)\\]$") == true) // dictionary + } + + func test_logMessage() { + let logger = DLog() + + // Any + let text: Any = "some text" + XCTAssert(logger.debug("\(text)")?.match("some text") == true) + + // Error + let error = NSError(domain: "domain", code: 100, userInfo: [NSLocalizedDescriptionKey : "error"]) + XCTAssert(logger.error("\(error)")?.match("Error Domain=domain Code=100 \"error\" UserInfo=\\{NSLocalizedDescription=error\\}") == true) + XCTAssert(logger.error("\(error as Error)")?.match("Error Domain=domain Code=100 \"error\" UserInfo=\\{NSLocalizedDescription=error\\}") == true) + XCTAssert(logger.error("\(error.localizedDescription)")?.match("error") == true) + + // Enum + enum MyEnum { case one, two } + let myEnum = MyEnum.one + XCTAssert(logger.error("\(myEnum)")?.match("one") == true) + XCTAssert(logger.error("\(MyEnum.one)")?.match("one") == true) + + // OptionSet + XCTAssert(logger.error("\(NSCalendar.Options.matchLast)")?.match("NSCalendarOptions\\(rawValue: 8192\\)") == true) + + // Notification + let notification = Notification.Name.NSCalendarDayChanged + XCTAssert(logger.debug("\(notification.rawValue)")?.match("NSCalendarDayChanged") == true) + + // Array + let array = [1, 2, 3] + XCTAssert(logger.debug("\(array)")?.match("\\[1, 2, 3\\]") == true) + } + + func test_Privacy() { + let logger = DLog() + + let empty = "" + let cardNumber = "1234 5678 9012 3456" + let greeting = "Hello World!" + let number = 1234567890 + + XCTAssert(logger.log("Default: \(cardNumber)")?.match(cardNumber) == true) + XCTAssert(logger.log("Public: \(cardNumber, privacy: .public)")?.match(cardNumber) == true) + + XCTAssert(logger.log("Private: \(cardNumber, privacy: .private)")?.match("") == true) + + XCTAssert(logger.log("Private hash: \(cardNumber, privacy: .private(mask: .hash))")?.match("[0-9a-fA-F]+") == true) + XCTAssert(logger.log("Private hash: \(cardNumber, privacy: .private(mask: .hash, auto: false))")?.match("[0-9a-fA-F]+") == true) + + XCTAssert(logger.log("Private random: \(empty, privacy: .private(mask: .random))")?.match(": $") == true) + XCTAssert(logger.log("Private random: \(cardNumber, privacy: .private(mask: .random))")?.match(cardNumber) == false) + XCTAssert(logger.log("Private random: \(greeting, privacy: .private(mask: .random))")?.match(greeting) == false) + + XCTAssert(logger.log("Private redact: \(empty, privacy: .private(mask: .redact))")?.match(": $") == true) + XCTAssert(logger.log("Private redact: \(cardNumber, privacy: .private(mask: .redact))")?.match("0000 0000 0000 0000") == true) + XCTAssert(logger.log("Private redact: \(greeting, privacy: .private(mask: .redact))")?.match("XXXXX XXXXX!") == true) + + XCTAssert(logger.log("Private shuffle: \("1 2 3", privacy: .private(mask: .shuffle))")?.match("1 2 3") == true) + XCTAssert(logger.log("Private shuffle: \(cardNumber, privacy: .private(mask: .shuffle))")?.match(cardNumber) == false) + XCTAssert(logger.log("Private shuffle: \(greeting, privacy: .private(mask: .shuffle))")?.match(greeting) == false) + + XCTAssert(logger.log("Private custom: \(cardNumber, privacy: .private(mask: .custom(value: "")))")?.match("") == true) + + XCTAssert(logger.log("Private partial: \(empty, privacy: .private(mask: .partial(first: -1, last: -2)))")?.match(": $") == true) + XCTAssert(logger.log("Private partial: \(cardNumber, privacy: .private(mask: .partial(first: -1, last: -2)))")?.match("[\\*]{19}") == true) + XCTAssert(logger.log("Private partial: \(cardNumber, privacy: .private(mask: .partial(first: 0, last: 0)))")?.match("[\\*]{19}") == true) + XCTAssert(logger.log("Private partial: \(cardNumber, privacy: .private(mask: .partial(first: 1, last: 4)))")?.match("1[\\*]{14}3456") == true) + XCTAssert(logger.log("Private partial: \(cardNumber, privacy: .private(mask: .partial(first: 10, last: 10)))")?.match(cardNumber) == true) + + XCTAssert(logger.log("Private reduce: \(cardNumber, privacy: .private(mask: .reduce(length: -3)))")?.match("...") == true) + XCTAssert(logger.log("Private reduce: \(cardNumber, privacy: .private(mask: .reduce(length: 0)))")?.match("...") == true) + XCTAssert(logger.log("Private reduce: \(cardNumber, privacy: .private(mask: .reduce(length: 1)))")?.match("...6") == true) + XCTAssert(logger.log("Private reduce: \(cardNumber, privacy: .private(mask: .reduce(length: 2)))")?.match("1...6") == true) + XCTAssert(logger.log("Private reduce: \(cardNumber, privacy: .private(mask: .reduce(length: 7)))")?.match("123...3456") == true) + XCTAssert(logger.log("Private reduce: \(cardNumber, privacy: .private(mask: .reduce(length: 100)))")?.match(cardNumber) == true) + XCTAssert(logger.log("Private reduce: \(greeting, privacy: .private(mask: .reduce(length: 6)))")?.match("Hel...ld!") == true) + + XCTAssert(logger.log("Private reduce: \(number, privacy: .private(mask: .reduce(length: 6)))")?.match("123...890") == true) + } + + func test_DateFormat() { + let logger = DLog() + + let date = Date(timeIntervalSince1970: 1645026131) // 2022-02-16 15:42:11 +0000 + + // Default + XCTAssert(logger.log("\(date)")?.match("2022-02-16 15:42:11 \\+0000") == true) + + // Date only + XCTAssert(logger.log("\(date, format: .date(dateStyle: .short))")?.match("2/16/22") == true) + XCTAssert(logger.log("\(date, format: .date(dateStyle: .medium))")?.match("Feb 16, 2022") == true) + XCTAssert(logger.log("\(date, format: .date(dateStyle: .long))")?.match("February 16, 2022") == true) + XCTAssert(logger.log("\(date, format: .date(dateStyle: .full))")?.match("Wednesday, February 16, 2022") == true) + + // Time only + XCTAssert(logger.log("\(date, format: .date(timeStyle: .short))")?.match("3:42 PM") == true) + XCTAssert(logger.log("\(date, format: .date(timeStyle: .medium))")?.match("3:42:11 PM") == true) + XCTAssert(logger.log("\(date, format: .date(timeStyle: .long))")?.match("3:42:11 PM GMT") == true) + XCTAssert(logger.log("\(date, format: .date(timeStyle: .full))")?.match("3:42:11 PM Greenwich Mean Time") == true) + + // Both + XCTAssert(logger.log("\(date, format: .date())")?.match(Empty) == true) // Empty + XCTAssert(logger.log("\(date, format: .date(dateStyle: .medium, timeStyle: .short))")?.match("Feb 16, 2022 at 3:42 PM") == true) + + // Custom + XCTAssert(logger.log("\(date, format: .dateCustom(format: "dd-MM-yyyy"))")?.match("16-02-2022") == true) + + // Privacy + XCTAssert(logger.log("\(date, format: .date(dateStyle: .short), privacy: .private(mask: .redact))")?.match("0/00/00") == true) + + // Locale + let locale = Locale(identifier: "en_GB") + XCTAssert(logger.log("\(date, format: .date(dateStyle: .medium, timeStyle: .short, locale: locale))")?.match("16 Feb 2022 at 15:42") == true) + } + + func test_IntFormat() { + let logger = DLog() + + let value = 20_234_557 + + // Default + XCTAssert(logger.log("\(value)")?.match("20234557") == true) + + // Binary + XCTAssert(logger.log("\(8, format: .binary)")?.match("1000") == true) + + // Octal + XCTAssert(logger.log("\(10, format: .octal)")?.match("12") == true) + XCTAssert(logger.log("\(10, format: .octal(includePrefix: true))")?.match("0o12") == true) + + // Hex + XCTAssert(logger.log("\(value, format: .hex)")?.match("134c13d") == true) + XCTAssert(logger.log("\(value, format: .hex(includePrefix: true))")?.match("0x134c13d") == true) + XCTAssert(logger.log("\(value, format: .hex(uppercase: true))")?.match("134C13D") == true) + XCTAssert(logger.log("\(value, format: .hex(includePrefix: true, uppercase: true))")?.match("0x134C13D") == true) + + // Byte count + + // Count style + XCTAssert(logger.log("\(1000, format: .byteCount)")?.match("1 KB") == true) + + XCTAssert(logger.log("\(value, format: .byteCount(countStyle: .file))")?.match("20.2 MB") == true) + XCTAssert(logger.log("\(value, format: .byteCount(countStyle: .memory))")?.match("19.3 MB") == true) + XCTAssert(logger.log("\(value, format: .byteCount(countStyle: .decimal))")?.match("20.2 MB") == true) + XCTAssert(logger.log("\(value, format: .byteCount(countStyle: .binary))")?.match("19.3 MB") == true) + + // Allowed Units + XCTAssert(logger.log("\(value, format: .byteCount(allowedUnits: .useBytes))")?.match("20,234,557 bytes") == true) + XCTAssert(logger.log("\(value, format: .byteCount(allowedUnits: .useKB))")?.match("20,235 KB") == true) + XCTAssert(logger.log("\(value, format: .byteCount(allowedUnits: .useGB))")?.match("0.02 GB") == true) + + // Both + XCTAssert(logger.log("\(value, format: .byteCount(countStyle: .memory, allowedUnits: .useGB))")?.match("0.02 GB") == true) + + // Privacy + XCTAssert(logger.log("\(value, format: .byteCount(allowedUnits: .useMB), privacy: .private(mask: .redact))")?.match("00.0 XX") == true) + + // Number + let number = 1_234 + XCTAssert(logger.log("\(number)")?.match("\(number)") == true) + XCTAssert(logger.log("\(number, format: .number)")?.match("1,234") == true) + XCTAssert(logger.log("\(number, format: .number(style: .none))")?.match("\(number)") == true) + XCTAssert(logger.log("\(number, format: .number(style: .decimal))")?.match("1,234") == true) + XCTAssert(logger.log("\(number, format: .number(style: .currency))")?.match("\\$1,234\\.00") == true) + XCTAssert(logger.log("\(number, format: .number(style: .percent))")?.match("123,400%") == true) + XCTAssert(logger.log("\(number, format: .number(style: .scientific))")?.match("1.234E3") == true) + XCTAssert(logger.log("\(number, format: .number(style: .spellOut))")?.match("one thousand two hundred thirty-four") == true) + + // Privacy + XCTAssert(logger.log("\(number, format: .number(style: .decimal), privacy: .private(mask: .redact))")?.match("0,000") == true) + + // Locale + let locale = Locale(identifier: "en_GB") + XCTAssert(logger.log("\(number, format: .number(style: .currency, locale: locale))")?.match("\\£1,234\\.00") == true) + // Number + + // HTTP + XCTAssert(logger.log("\(200, format: .httpStatusCode)")?.match("HTTP 200 no error") == true) + XCTAssert(logger.log("\(400, format: .httpStatusCode)")?.match("HTTP 400 bad request") == true) + XCTAssert(logger.log("\(404, format: .httpStatusCode)")?.match("HTTP 404 not found") == true) + XCTAssert(logger.log("\(500, format: .httpStatusCode)")?.match("HTTP 500 internal server error") == true) + + // IPv4 + let ip4 = 0x0100007f // 16777343 + XCTAssert(logger.log("\(0, format: .ipv4Address)")?.match("0.0.0.0") == true) + XCTAssert(logger.log("\(ip4, format: .ipv4Address)")?.match("127.0.0.1") == true) + XCTAssert(logger.log("\(-ip4, format: .ipv4Address)")?.match("127.0.0.1") == false) + XCTAssert(logger.log("\(0x0101a8c0, format: .ipv4Address)")?.match("192.168.1.1") == true) + + // Time + let time = 60 * 60 + 23 * 60 + 15 + XCTAssert(logger.log("\(time, format: .time)")?.match("1h 23m 15s") == true) + XCTAssert(logger.log("\(time, format: .time(unitsStyle: .positional))")?.match("1:23:15") == true) + XCTAssert(logger.log("\(time, format: .time(unitsStyle: .short))")?.match("1 hr, 23 min, 15 sec") == true) + XCTAssert(logger.log("\(time, format: .time(unitsStyle: .full))")?.match("1 hour, 23 minutes, 15 seconds") == true) + XCTAssert(logger.log("\(time, format: .time(unitsStyle: .spellOut))")?.match("one hour, twenty-three minutes, fifteen seconds") == true) + + // Date + let timeIntervalSince1970 = 1645026131 // 2022-02-16 15:42:11 +0000 + XCTAssert(logger.log("\(timeIntervalSince1970, format: .date)")?.match("2/16/22, 3:42 PM$") == true) + XCTAssert(logger.log("\(timeIntervalSince1970, format: .date(dateStyle: .short))")?.match("2/16/22$") == true) + XCTAssert(logger.log("\(timeIntervalSince1970, format: .date(timeStyle: .medium))")?.match("3:42:11 PM$") == true) + XCTAssert(logger.log("\(timeIntervalSince1970, format: .date(dateStyle: .short, timeStyle: .short, locale: locale))")?.match("16/02/2022, 15:42$") == true) + } + + func test_DoubleFormat() { + let logger = DLog() + + let value = 12.345 + + // Default + XCTAssert(logger.log("\(value)")?.match("12.345") == true) + + // Fixed + XCTAssert(logger.log("\(value, format: .fixed)")?.match("12.345000") == true) + XCTAssert(logger.log("\(value, format: .fixed(precision: 2))")?.match("12.35") == true) + + // Hex + XCTAssert(logger.log("\(value, format: .hex)")?.match("1\\.8b0a3d70a3d71p\\+3") == true) + XCTAssert(logger.log("\(value, format: .hex(includePrefix: true))")?.match("0x1\\.8b0a3d70a3d71p\\+3") == true) + XCTAssert(logger.log("\(value, format: .hex(uppercase: true))")?.match("1\\.8B0A3D70A3D71P\\+3") == true) + XCTAssert(logger.log("\(value, format: .hex(includePrefix: true, uppercase: true))")?.match("0x1\\.8B0A3D70A3D71P\\+3") == true) + + // Exponential + XCTAssert(logger.log("\(value, format: .exponential)")?.match("1\\.234500e\\+01") == true) + XCTAssert(logger.log("\(value, format: .exponential(precision: 2))")?.match("1\\.23e\\+01") == true) + + // Hybrid + XCTAssert(logger.log("\(value, format: .hybrid)")?.match("12.345") == true) + XCTAssert(logger.log("\(value, format: .hybrid(precision: 1))")?.match("1e\\+01") == true) + + // Privacy + XCTAssert(logger.log("\(value, format: .hybrid(precision: 1), privacy: .private(mask: .redact))")?.match("0X\\+00") == true) + + // Number + let number = 1_234.56 + XCTAssert(logger.log("\(number)")?.match("\(number)") == true) + + XCTAssert(logger.log("\(number, format: .number(style: .none))")?.match("1235") == true) + XCTAssert(logger.log("\(number, format: .number(style: .decimal))")?.match("1,234.56") == true) + XCTAssert(logger.log("\(number, format: .number(style: .currency))")?.match("\\$1,234\\.56") == true) + XCTAssert(logger.log("\(number, format: .number(style: .percent))")?.match("123,456%") == true) + XCTAssert(logger.log("\(number, format: .number(style: .scientific))")?.match("1.23456E3") == true) + XCTAssert(logger.log("\(number, format: .number(style: .spellOut))")?.match("one thousand two hundred thirty-four point five six") == true) + + // Privacy + XCTAssert(logger.log("\(number, format: .number(style: .decimal), privacy: .private(mask: .redact))")?.match("0,000.00") == true) + + // Locale + let locale = Locale(identifier: "en_GB") + XCTAssert(logger.log("\(number, format: .number(style: .currency, locale: locale))")?.match("\\£1,234\\.56") == true) + // Number + + // Time + let durationWithSecs = 60 * 60 + 23 * 60 + 1.25 + XCTAssert(logger.log("\(durationWithSecs, format: .time)")?.match("1h 23m 1.250s$") == true) + XCTAssert(logger.log("\(durationWithSecs, format: .time(unitsStyle: .positional))")?.match("1:23:01.250$") == true) + XCTAssert(logger.log("\(durationWithSecs, format: .time(unitsStyle: .short))")?.match("1 hr, 23 min, 1.250 sec$") == true) + XCTAssert(logger.log("\(durationWithSecs, format: .time(unitsStyle: .full))")?.match("1 hour, 23 minutes, 1.250 second$") == true) + XCTAssert(logger.log("\(durationWithSecs, format: .time(unitsStyle: .spellOut))")?.match("one hour, twenty-three minutes, one second, two hundred fifty milliseconds$") == true) + XCTAssert(logger.log("\(durationWithSecs, format: .time(unitsStyle: .brief))")?.match("1hr 23min 1.250sec$") == true) + + let durationNoSecs = 60 * 60 + 23 * 60 + let durationWithMs = 60 * 60 + 23 * 60 + 0.45 + XCTAssert(logger.log("\(durationNoSecs, format: .time)")?.match("1h 23m$") == true) + XCTAssert(logger.log("\(durationWithMs, format: .time)")?.match("1h 23m 0.450s$") == true) + + // Date + let dateWithMin = 1645026131.45 // 2022-02-16 15:42:11 +0000 + XCTAssert(logger.log("\(dateWithMin, format: .date)")?.match("2/16/22, 3:42 PM$") == true) + XCTAssert(logger.log("\(dateWithMin, format: .date(dateStyle: .short))")?.match("2/16/22$") == true) + XCTAssert(logger.log("\(dateWithMin, format: .date(timeStyle: .medium))")?.match("3:42:11 PM$") == true) + XCTAssert(logger.log("\(dateWithMin, format: .date(dateStyle: .short, timeStyle: .short, locale: locale))")?.match("16/02/2022, 15:42$") == true) + } + + func test_BoolFormat() { + + let logger = DLog() + + let value = true + + // Default + XCTAssert(logger.log("\(value)")?.match("true") == true) + + // Binary + XCTAssert(logger.log("\(value, format: .binary)")?.match("1") == true) + XCTAssert(logger.log("\(!value, format: .binary)")?.match("0") == true) + + // Answer + XCTAssert(logger.log("\(value, format: .answer)")?.match("yes") == true) + XCTAssert(logger.log("\(!value, format: .answer)")?.match("no") == true) + + // Toggle + XCTAssert(logger.log("\(value, format: .toggle)")?.match("on") == true) + XCTAssert(logger.log("\(!value, format: .toggle)")?.match("off") == true) + } + + func test_DataFormat() { + let logger = DLog() + + // IPv6 + let ipString = "2001:0b28:f23f:f005:0000:0000:0000:000a" + let ipv6 = IPv6Address(ipString)! + XCTAssert(logger.log("\(ipv6.rawValue, format: .ipv6Address)")?.match("2001:b28:f23f:f005::a$") == true) + XCTAssert(logger.log("\(Data([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), format: .ipv6Address)")?.match(Empty) == true) + + // Text + let text = "Hello DLog!" + var data = text.data(using: .utf8)! + XCTAssert(logger.log("\(data, format: .text)")?.match(text) == true) + XCTAssert(logger.log("\(Data([255, 2, 3, 4, 5, 6, 7, 8, 9]), format: .text)")?.match(Empty) == true) + + // UUID + let uuid = UUID() + var tuple = uuid.uuid + data = withUnsafeBytes(of: &tuple) { Data($0) } + XCTAssert(logger.log("\(data, format: .uuid)")?.match(uuid.uuidString) == true) + XCTAssert(logger.log("\(Data([0, 1, 2, 3]), format: .uuid)")?.match(Empty) == true) + + // Raw + data = Data([0xab, 0xcd, 0xef]) + XCTAssert(logger.log("\(data, format: .raw)")?.match("ABCDEF") == true) + } + + func test_FormatConcurent() { + let logger = DLog() + + for _ in 0...20 { + DispatchQueue.global().async { + let date = Date(timeIntervalSince1970: 1645026131) // 2022-02-16 15:42:11 +0000 XCTAssert(logger.log("\(date, format: .date(dateStyle: .short))")?.match("2/16/22") == true) - XCTAssert(logger.log("\(date, format: .date(dateStyle: .medium))")?.match("Feb 16, 2022") == true) - XCTAssert(logger.log("\(date, format: .date(dateStyle: .long))")?.match("February 16, 2022") == true) - XCTAssert(logger.log("\(date, format: .date(dateStyle: .full))")?.match("Wednesday, February 16, 2022") == true) - // Time only - XCTAssert(logger.log("\(date, format: .date(timeStyle: .short))")?.match("3:42 PM") == true) - XCTAssert(logger.log("\(date, format: .date(timeStyle: .medium))")?.match("3:42:11 PM") == true) - XCTAssert(logger.log("\(date, format: .date(timeStyle: .long))")?.match("3:42:11 PM GMT") == true) - XCTAssert(logger.log("\(date, format: .date(timeStyle: .full))")?.match("3:42:11 PM Greenwich Mean Time") == true) + let number = 1_234_567_890 + XCTAssert(logger.log("\(number, format: .number(style: .none))")?.match("\(number)") == true) - // Both - XCTAssert(logger.log("\(date, format: .date())")?.match(Empty) == true) // Empty - XCTAssert(logger.log("\(date, format: .date(dateStyle: .medium, timeStyle: .short))")?.match("Feb 16, 2022 at 3:42 PM") == true) + let value: Int64 = 20_234_557 + XCTAssert(logger.log("\(value, format: .byteCount(countStyle: .file))")?.match("20.2 MB") == true) + } + } + } +} - // Custom - XCTAssert(logger.log("\(date, format: .dateCustom(format: "dd-MM-yyyy"))")?.match("16-02-2022") == true) - - // Privacy - XCTAssert(logger.log("\(date, format: .date(dateStyle: .short), privacy: .private(mask: .redact))")?.match("0/00/00") == true) - - // Locale - let locale = Locale(identifier: "en_GB") - XCTAssert(logger.log("\(date, format: .date(dateStyle: .medium, timeStyle: .short, locale: locale))")?.match("16 Feb 2022 at 15:42") == true) +final class IntervalTests: XCTestCase { + + func test_Interval() { + let logger = DLog() + + XCTAssert(read_stdout { + logger.interval("signpost") { + delay() + } + }?.match(#"\#(Interval) signpost$"#) == true) + } + + func test_IntervalBeginEnd() { + let logger = DLog() + + XCTAssert(read_stdout { + let interval = logger.interval("signpost") + interval.begin() + delay() + interval.end() + }?.match(#"\#(Interval) signpost$"#) == true) + + // Double begin/end + XCTAssert(read_stdout { + let interval = logger.interval("signpost") + interval.begin() + interval.begin() + delay() + interval.end() + interval.end() + }?.match(#"\#(Interval) signpost$"#) == true) + } + + func test_IntervalStatistics() { + let logger = DLog() + + let interval = logger.interval("Signpost") { + delay() } + let statistics1 = interval.statistics + XCTAssert(statistics1.count == 1) + XCTAssert(0.25 <= interval.duration) + XCTAssert(0.25 <= statistics1.total) + XCTAssert(0.25 <= statistics1.min) + XCTAssert(0.25 <= statistics1.max) + XCTAssert(0.25 <= statistics1.average) - func test_IntFormat() { - let logger = DLog() - - let value = 20_234_557 - - // Default - XCTAssert(logger.log("\(value)")?.match("20234557") == true) - - // Binary - XCTAssert(logger.log("\(8, format: .binary)")?.match("1000") == true) - - // Octal - XCTAssert(logger.log("\(10, format: .octal)")?.match("12") == true) - XCTAssert(logger.log("\(10, format: .octal(includePrefix: true))")?.match("0o12") == true) - - // Hex - XCTAssert(logger.log("\(value, format: .hex)")?.match("134c13d") == true) - XCTAssert(logger.log("\(value, format: .hex(includePrefix: true))")?.match("0x134c13d") == true) - XCTAssert(logger.log("\(value, format: .hex(uppercase: true))")?.match("134C13D") == true) - XCTAssert(logger.log("\(value, format: .hex(includePrefix: true, uppercase: true))")?.match("0x134C13D") == true) - - // Byte count - - // Count style - XCTAssert(logger.log("\(1000, format: .byteCount)")?.match("1 KB") == true) - - XCTAssert(logger.log("\(value, format: .byteCount(countStyle: .file))")?.match("20.2 MB") == true) - XCTAssert(logger.log("\(value, format: .byteCount(countStyle: .memory))")?.match("19.3 MB") == true) - XCTAssert(logger.log("\(value, format: .byteCount(countStyle: .decimal))")?.match("20.2 MB") == true) - XCTAssert(logger.log("\(value, format: .byteCount(countStyle: .binary))")?.match("19.3 MB") == true) - - // Allowed Units - XCTAssert(logger.log("\(value, format: .byteCount(allowedUnits: .useBytes))")?.match("20,234,557 bytes") == true) - XCTAssert(logger.log("\(value, format: .byteCount(allowedUnits: .useKB))")?.match("20,235 KB") == true) - XCTAssert(logger.log("\(value, format: .byteCount(allowedUnits: .useGB))")?.match("0.02 GB") == true) - - // Both - XCTAssert(logger.log("\(value, format: .byteCount(countStyle: .memory, allowedUnits: .useGB))")?.match("0.02 GB") == true) - - // Privacy - XCTAssert(logger.log("\(value, format: .byteCount(allowedUnits: .useMB), privacy: .private(mask: .redact))")?.match("00.0 XX") == true) - - // Number - let number = 1_234 - XCTAssert(logger.log("\(number)")?.match("\(number)") == true) - XCTAssert(logger.log("\(number, format: .number)")?.match("1,234") == true) - XCTAssert(logger.log("\(number, format: .number(style: .none))")?.match("\(number)") == true) - XCTAssert(logger.log("\(number, format: .number(style: .decimal))")?.match("1,234") == true) - XCTAssert(logger.log("\(number, format: .number(style: .currency))")?.match("\\$1,234\\.00") == true) - XCTAssert(logger.log("\(number, format: .number(style: .percent))")?.match("123,400%") == true) - XCTAssert(logger.log("\(number, format: .number(style: .scientific))")?.match("1.234E3") == true) - XCTAssert(logger.log("\(number, format: .number(style: .spellOut))")?.match("one thousand two hundred thirty-four") == true) - - // Privacy - XCTAssert(logger.log("\(number, format: .number(style: .decimal), privacy: .private(mask: .redact))")?.match("0,000") == true) - - // Locale - let locale = Locale(identifier: "en_GB") - XCTAssert(logger.log("\(number, format: .number(style: .currency, locale: locale))")?.match("\\£1,234\\.00") == true) - // Number - - // HTTP - XCTAssert(logger.log("\(200, format: .httpStatusCode)")?.match("HTTP 200 no error") == true) - XCTAssert(logger.log("\(400, format: .httpStatusCode)")?.match("HTTP 400 bad request") == true) - XCTAssert(logger.log("\(404, format: .httpStatusCode)")?.match("HTTP 404 not found") == true) - XCTAssert(logger.log("\(500, format: .httpStatusCode)")?.match("HTTP 500 internal server error") == true) - - // IPv4 - let ip4 = 0x0100007f // 16777343 - XCTAssert(logger.log("\(0, format: .ipv4Address)")?.match("0.0.0.0") == true) - XCTAssert(logger.log("\(ip4, format: .ipv4Address)")?.match("127.0.0.1") == true) - XCTAssert(logger.log("\(-ip4, format: .ipv4Address)")?.match("127.0.0.1") == false) - XCTAssert(logger.log("\(0x0101a8c0, format: .ipv4Address)")?.match("192.168.1.1") == true) - - // Time - let time = 60 * 60 + 23 * 60 + 15 - XCTAssert(logger.log("\(time, format: .time)")?.match("1h 23m 15s") == true) - XCTAssert(logger.log("\(time, format: .time(unitsStyle: .positional))")?.match("1:23:15") == true) - XCTAssert(logger.log("\(time, format: .time(unitsStyle: .short))")?.match("1 hr, 23 min, 15 sec") == true) - XCTAssert(logger.log("\(time, format: .time(unitsStyle: .full))")?.match("1 hour, 23 minutes, 15 seconds") == true) - XCTAssert(logger.log("\(time, format: .time(unitsStyle: .spellOut))")?.match("one hour, twenty-three minutes, fifteen seconds") == true) - - // Date - let timeIntervalSince1970 = 1645026131 // 2022-02-16 15:42:11 +0000 - XCTAssert(logger.log("\(timeIntervalSince1970, format: .date)")?.match("2/16/22, 3:42 PM$") == true) - XCTAssert(logger.log("\(timeIntervalSince1970, format: .date(dateStyle: .short))")?.match("2/16/22$") == true) - XCTAssert(logger.log("\(timeIntervalSince1970, format: .date(timeStyle: .medium))")?.match("3:42:11 PM$") == true) - XCTAssert(logger.log("\(timeIntervalSince1970, format: .date(dateStyle: .short, timeStyle: .short, locale: locale))")?.match("16/02/2022, 15:42$") == true) + interval.begin() + delay() + interval.end() + let statistics2 = interval.statistics + XCTAssert(statistics2.count == 2) + XCTAssert(0.25 <= interval.duration) + XCTAssert(0.5 <= statistics2.total) + XCTAssert(0.25 <= statistics2.min) + XCTAssert(0.25 <= statistics2.max) + XCTAssert(0.25 <= statistics2.average) + } + + func test_IntervalConcurrent() { + var config = LogConfig() + config.intervalConfig.options = .all + let logger = DLog(config: config) + + wait(count: 10) { expectations in + for i in 0..<10 { + DispatchQueue.global().async { + let interval = logger.interval("signpost") { + delay(); + } + XCTAssert(interval.duration >= 0.25) + expectations[i].fulfill() + } + } } + } + + func test_IntervalNameEmpty() { + let logger = DLog() - func test_DoubleFormat() { - let logger = DLog() - - let value = 12.345 - - // Default - XCTAssert(logger.log("\(value)")?.match("12.345") == true) - - // Fixed - XCTAssert(logger.log("\(value, format: .fixed)")?.match("12.345000") == true) - XCTAssert(logger.log("\(value, format: .fixed(precision: 2))")?.match("12.35") == true) - - // Hex - XCTAssert(logger.log("\(value, format: .hex)")?.match("1\\.8b0a3d70a3d71p\\+3") == true) - XCTAssert(logger.log("\(value, format: .hex(includePrefix: true))")?.match("0x1\\.8b0a3d70a3d71p\\+3") == true) - XCTAssert(logger.log("\(value, format: .hex(uppercase: true))")?.match("1\\.8B0A3D70A3D71P\\+3") == true) - XCTAssert(logger.log("\(value, format: .hex(includePrefix: true, uppercase: true))")?.match("0x1\\.8B0A3D70A3D71P\\+3") == true) - - // Exponential - XCTAssert(logger.log("\(value, format: .exponential)")?.match("1\\.234500e\\+01") == true) - XCTAssert(logger.log("\(value, format: .exponential(precision: 2))")?.match("1\\.23e\\+01") == true) - - // Hybrid - XCTAssert(logger.log("\(value, format: .hybrid)")?.match("12.345") == true) - XCTAssert(logger.log("\(value, format: .hybrid(precision: 1))")?.match("1e\\+01") == true) - - // Privacy - XCTAssert(logger.log("\(value, format: .hybrid(precision: 1), privacy: .private(mask: .redact))")?.match("0X\\+00") == true) - - // Number - let number = 1_234.56 - XCTAssert(logger.log("\(number)")?.match("\(number)") == true) - - XCTAssert(logger.log("\(number, format: .number(style: .none))")?.match("1235") == true) - XCTAssert(logger.log("\(number, format: .number(style: .decimal))")?.match("1,234.56") == true) - XCTAssert(logger.log("\(number, format: .number(style: .currency))")?.match("\\$1,234\\.56") == true) - XCTAssert(logger.log("\(number, format: .number(style: .percent))")?.match("123,456%") == true) - XCTAssert(logger.log("\(number, format: .number(style: .scientific))")?.match("1.23456E3") == true) - XCTAssert(logger.log("\(number, format: .number(style: .spellOut))")?.match("one thousand two hundred thirty-four point five six") == true) - - // Privacy - XCTAssert(logger.log("\(number, format: .number(style: .decimal), privacy: .private(mask: .redact))")?.match("0,000.00") == true) - - // Locale - let locale = Locale(identifier: "en_GB") - XCTAssert(logger.log("\(number, format: .number(style: .currency, locale: locale))")?.match("\\£1,234\\.56") == true) - // Number - - // Time - let durationWithSecs = 60 * 60 + 23 * 60 + 1.25 - XCTAssert(logger.log("\(durationWithSecs, format: .time)")?.match("1h 23m 1.250s$") == true) - XCTAssert(logger.log("\(durationWithSecs, format: .time(unitsStyle: .positional))")?.match("1:23:01.250$") == true) - XCTAssert(logger.log("\(durationWithSecs, format: .time(unitsStyle: .short))")?.match("1 hr, 23 min, 1.250 sec$") == true) - XCTAssert(logger.log("\(durationWithSecs, format: .time(unitsStyle: .full))")?.match("1 hour, 23 minutes, 1.250 second$") == true) - XCTAssert(logger.log("\(durationWithSecs, format: .time(unitsStyle: .spellOut))")?.match("one hour, twenty-three minutes, one second, two hundred fifty milliseconds$") == true) - XCTAssert(logger.log("\(durationWithSecs, format: .time(unitsStyle: .brief))")?.match("1hr 23min 1.250sec$") == true) - - let durationNoSecs = 60 * 60 + 23 * 60 - let durationWithMs = 60 * 60 + 23 * 60 + 0.45 - XCTAssert(logger.log("\(durationNoSecs, format: .time)")?.match("1h 23m$") == true) - XCTAssert(logger.log("\(durationWithMs, format: .time)")?.match("1h 23m 0.450s$") == true) - - // Date - let dateWithMin = 1645026131.45 // 2022-02-16 15:42:11 +0000 - XCTAssert(logger.log("\(dateWithMin, format: .date)")?.match("2/16/22, 3:42 PM$") == true) - XCTAssert(logger.log("\(dateWithMin, format: .date(dateStyle: .short))")?.match("2/16/22$") == true) - XCTAssert(logger.log("\(dateWithMin, format: .date(timeStyle: .medium))")?.match("3:42:11 PM$") == true) - XCTAssert(logger.log("\(dateWithMin, format: .date(dateStyle: .short, timeStyle: .short, locale: locale))")?.match("16/02/2022, 15:42$") == true) + XCTAssert(read_stdout { + logger.interval("") { + delay() + } + }?.match(#"\#(Interval)$"#) == true) + } + + func test_IntervalConfigEmpty() { + var config = LogConfig() + config.intervalConfig.options = [] + + let logger = DLog(config: config) + + XCTAssert(read_stdout { + logger.interval("signpost") { + delay() + } + }?.match(#"> signpost$"#) == true) + } + + func test_IntervalConfigAll() { + var config = LogConfig() + config.intervalConfig.options = .all + + let logger = DLog(config: config) + + XCTAssert(read_stdout { + logger.interval("signpost") { + delay() + } + }?.match(#"\{average:\#(SECS),count:[0-9]+,duration:\#(SECS),max:\#(SECS),min:\#(SECS),total:\#(SECS)\} signpost$"#) == true) + } +} + +final class ScopeTests: XCTestCase { + + func test_scope() { + let logger = DLog() + + logger.scope("scope") { + testAll($0) } + } + + func test_scope_config_empty() { + var config = LogConfig() + config.options = [] + let logger = DLog(config: config) - func test_BoolFormat() { - - let logger = DLog() - - let value = true - - // Default - XCTAssert(logger.log("\(value)")?.match("true") == true) - - // Binary - XCTAssert(logger.log("\(value, format: .binary)")?.match("1") == true) - XCTAssert(logger.log("\(!value, format: .binary)")?.match("0") == true) - - // Answer - XCTAssert(logger.log("\(value, format: .answer)")?.match("yes") == true) - XCTAssert(logger.log("\(!value, format: .answer)")?.match("no") == true) - - // Toggle - XCTAssert(logger.log("\(value, format: .toggle)")?.match("on") == true) - XCTAssert(logger.log("\(!value, format: .toggle)")?.match("off") == true) + logger.scope("scope") { + XCTAssert($0.trace()?.match(#"\{func:test_scope_config_empty\(\),thread:\{name:main,number:1\}\}$"#) == true) } + } + + func test_scope_stack() { + var config = LogConfig() + config.options = .all - func test_DataFormat() { - let logger = DLog() - - // IPv6 - let ipString = "2001:0b28:f23f:f005:0000:0000:0000:000a" - let ipv6 = IPv6Address(ipString)! - XCTAssert(logger.log("\(ipv6.rawValue, format: .ipv6Address)")?.match("2001:b28:f23f:f005::a$") == true) - XCTAssert(logger.log("\(Data([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), format: .ipv6Address)")?.match(Empty) == true) - - // Text - let text = "Hello DLog!" - var data = text.data(using: .utf8)! - XCTAssert(logger.log("\(data, format: .text)")?.match(text) == true) - XCTAssert(logger.log("\(Data([255, 2, 3, 4, 5, 6, 7, 8, 9]), format: .text)")?.match(Empty) == true) + let logger = DLog(config: config) + + XCTAssert(logger.debug("no scope")?.match(#"\[00\] \#(CategoryTag) \#(DebugTag) \#(Location) no scope"#) == true) + + logger.scope("scope1") { scope1 in + XCTAssert(scope1.info("scope1 start")?.match(#"\[01\] \#(CategoryTag) ├ \#(InfoTag) \#(Location) scope1 start"#) == true) + + logger.scope("scope2") { scope2 in + XCTAssert(scope2.debug("scope2 start")?.match(#"\[02\] \#(CategoryTag) │ ├ \#(DebugTag) \#(Location) scope2 start"#) == true) - // UUID - let uuid = UUID() - var tuple = uuid.uuid - data = withUnsafeBytes(of: &tuple) { Data($0) } - XCTAssert(logger.log("\(data, format: .uuid)")?.match(uuid.uuidString) == true) - XCTAssert(logger.log("\(Data([0, 1, 2, 3]), format: .uuid)")?.match(Empty) == true) + logger.scope("scope3") { scope3 in + XCTAssert(scope3.error("scope3")?.match(#"\[03\] \#(CategoryTag) │ │ ├ \#(ErrorTag) \#(Location) scope3"#) == true) + } - // Raw - data = Data([0xab, 0xcd, 0xef]) - XCTAssert(logger.log("\(data, format: .raw)")?.match("ABCDEF") == true) + XCTAssert(scope2.fault("scope2")?.match(#"\[02\] \#(CategoryTag) │ ├ \#(FaultTag) \#(Location) scope2"#) == true) + } + + XCTAssert(scope1.trace("scope1 end")?.match(#"\[01\] \#(CategoryTag) ├ \#(TraceTag) \#(Location) \{func:test_scope_stack\(\),thread:\{name:main,number:1\}\} scope1 end$"#) == true) } - func test_FormatConcurent() { - let logger = DLog() - - for _ in 0...20 { - DispatchQueue.global().async { - let date = Date(timeIntervalSince1970: 1645026131) // 2022-02-16 15:42:11 +0000 - XCTAssert(logger.log("\(date, format: .date(dateStyle: .short))")?.match("2/16/22") == true) - - let number = 1_234_567_890 - XCTAssert(logger.log("\(number, format: .number(style: .none))")?.match("\(number)") == true) - - let value: Int64 = 20_234_557 - XCTAssert(logger.log("\(value, format: .byteCount(countStyle: .file))")?.match("20.2 MB") == true) - } + XCTAssert(logger.trace("no scope")?.match(#"\[00\] \#(CategoryTag) \#(TraceTag) \#(Location) \{func:test_scope_stack\(\),thread:\{name:main,number:1\}\} no scope$"#) == true) + } + + func test_scope_not_entered() { + let logger = DLog() + let scope1 = logger.scope("scope 1") + XCTAssert(scope1.trace()?.match(#"\#(CategoryTag) \#(TraceTag) \#(Location) \{func:\#(#function)"#) == true) + } + + func test_scope_enter_leave() { + let logger = DLog() + + let scope1 = logger.scope("scope 1") + let scope2 = logger.scope("scope 2") + let scope3 = logger.scope("scope 3") + + logger.trace("no scope") + + scope1.enter() + XCTAssert(scope1.info("1")?.match(#"\#(CategoryTag) ├ \#(InfoTag) \#(Location) 1"#) == true) + + scope2.enter() + XCTAssert(scope2.info("2")?.match(#"\#(CategoryTag) │ ├ \#(InfoTag) \#(Location) 2"#) == true) + + scope3.enter() + XCTAssert(scope3.info("3")?.match(#"\#(CategoryTag) │ │ ├ \#(InfoTag) \#(Location) 3"#) == true) + + scope1.leave() + XCTAssert(scope3.debug("3")?.match(#"\#(CategoryTag) │ ├ \#(DebugTag) \#(Location) 3"#) == true) + + scope2.leave() + XCTAssert(scope3.error("3")?.match(#"\#(CategoryTag) ├ \#(ErrorTag) \#(Location) 3"#) == true) + + scope3.leave() + XCTAssert(logger.fault("no scope")?.match(#"\#(CategoryTag) \#(FaultTag) \#(Location) no scope"#) == true) + } + + func test_scope_double_enter() { + let logger = DLog() + + let scope1 = logger.scope("My Scope") + + scope1.enter() + scope1.enter() + + XCTAssert(scope1.trace()?.match(#"\#(CategoryTag) ├ \#(TraceTag) \#(Location) \{func:\#(#function)"#) == true) + + scope1.leave() + scope1.leave() + + scope1.enter() + XCTAssert(scope1.trace()?.match(#"\#(CategoryTag) ├ \#(TraceTag) \#(Location) \{func:\#(#function)"#) == true) + scope1.leave() + + XCTAssert(logger.trace()?.match(#"\#(CategoryTag) \#(TraceTag) \#(Location) \{func:\#(#function)"#) == true) + } + + func test_scope_concurrent() { + let logger = DLog() + + wait(count: 10) { expectations in + for i in 1...10 { + DispatchQueue.global().async { + logger.scope("Scope \(i)") { + $0.debug("scope \(i)") + expectations[i-1].fulfill() + } } + } } -} - -final class IntervalTests: XCTestCase { - - func test_Interval() { - let logger = DLog() - - XCTAssert(read_stdout { - logger.interval("signpost") { - delay() - } - }?.match(#"\#(Interval) signpost$"#) == true) - } - - func test_IntervalBeginEnd() { - let logger = DLog() - - XCTAssert(read_stdout { - let interval = logger.interval("signpost") - interval.begin() - delay() - interval.end() - }?.match(#"\#(Interval) signpost$"#) == true) - - // Double begin/end - XCTAssert(read_stdout { - let interval = logger.interval("signpost") - interval.begin() - interval.begin() - delay() - interval.end() - interval.end() - }?.match(#"\#(Interval) signpost$"#) == true) - } - - func test_IntervalStatistics() { - let logger = DLog() - - let interval = logger.interval("Signpost") { - delay() - } - let statistics1 = interval.statistics - XCTAssert(statistics1.count == 1) - XCTAssert(0.25 <= interval.duration) - XCTAssert(0.25 <= statistics1.total) - XCTAssert(0.25 <= statistics1.min) - XCTAssert(0.25 <= statistics1.max) - XCTAssert(0.25 <= statistics1.average) - - interval.begin() - delay() - interval.end() - let statistics2 = interval.statistics - XCTAssert(statistics2.count == 2) - XCTAssert(0.25 <= interval.duration) - XCTAssert(0.5 <= statistics2.total) - XCTAssert(0.25 <= statistics2.min) - XCTAssert(0.25 <= statistics2.max) - XCTAssert(0.25 <= statistics2.average) - } - - func test_IntervalConcurrent() { - var config = LogConfig() - config.intervalConfig.options = .all - let logger = DLog(config: config) - - wait(count: 10) { expectations in - for i in 0..<10 { - DispatchQueue.global().async { - let interval = logger.interval("signpost") { - delay(); - } - XCTAssert(interval.duration >= 0.25) - expectations[i].fulfill() - } - } - } - } - - func test_IntervalNameEmpty() { - let logger = DLog() - - XCTAssert(read_stdout { - logger.interval("") { - delay() - } - }?.match(#"\#(Interval)$"#) == true) - } - - func test_IntervalConfigEmpty() { - var config = LogConfig() - config.intervalConfig.options = [] - - let logger = DLog(config: config) - - XCTAssert(read_stdout { - logger.interval("signpost") { - delay() - } - }?.match(#"> signpost$"#) == true) - } - - func test_IntervalConfigAll() { - var config = LogConfig() - config.intervalConfig.options = .all - - let logger = DLog(config: config) - - XCTAssert(read_stdout { - logger.interval("signpost") { - delay() - } - }?.match(#"\{average:\#(SECS),count:[0-9]+,duration:\#(SECS),max:\#(SECS),min:\#(SECS),total:\#(SECS)\} signpost$"#) == true) - } -} - -final class ScopeTests: XCTestCase { - - func test_scope() { - let logger = DLog() - - logger.scope("scope") { - testAll($0) - } - } - - func test_scope_config_empty() { - var config = LogConfig() - config.options = [] - let logger = DLog(config: config) - - logger.scope("scope") { - XCTAssert($0.trace()?.match(#"\{func:test_scope_config_empty\(\),thread:\{name:main,number:1\}\}$"#) == true) - } - } - - func test_scope_stack() { - var config = LogConfig() - config.options = .all - - let logger = DLog(config: config) - - XCTAssert(logger.debug("no scope")?.match(#"\[00\] \#(CategoryTag) \#(DebugTag) \#(Location) no scope"#) == true) - - logger.scope("scope1") { scope1 in - XCTAssert(scope1.info("scope1 start")?.match(#"\[01\] \#(CategoryTag) ├ \#(InfoTag) \#(Location) scope1 start"#) == true) - - logger.scope("scope2") { scope2 in - XCTAssert(scope2.debug("scope2 start")?.match(#"\[02\] \#(CategoryTag) │ ├ \#(DebugTag) \#(Location) scope2 start"#) == true) - - logger.scope("scope3") { scope3 in - XCTAssert(scope3.error("scope3")?.match(#"\[03\] \#(CategoryTag) │ │ ├ \#(ErrorTag) \#(Location) scope3"#) == true) - } - - XCTAssert(scope2.fault("scope2")?.match(#"\[02\] \#(CategoryTag) │ ├ \#(FaultTag) \#(Location) scope2"#) == true) - } - - XCTAssert(scope1.trace("scope1 end")?.match(#"\[01\] \#(CategoryTag) ├ \#(TraceTag) \#(Location) \{func:test_scope_stack\(\),thread:\{name:main,number:1\}\} scope1 end$"#) == true) - } - - XCTAssert(logger.trace("no scope")?.match(#"\[00\] \#(CategoryTag) \#(TraceTag) \#(Location) \{func:test_scope_stack\(\),thread:\{name:main,number:1\}\} no scope$"#) == true) - } - - func test_scope_not_entered() { - let logger = DLog() - let scope1 = logger.scope("scope 1") - XCTAssert(scope1.trace()?.match(#"\#(CategoryTag) \#(TraceTag) \#(Location) \{func:\#(#function)"#) == true) - } - - func test_scope_enter_leave() { - let logger = DLog() - - let scope1 = logger.scope("scope 1") - let scope2 = logger.scope("scope 2") - let scope3 = logger.scope("scope 3") - - logger.trace("no scope") - - scope1.enter() - XCTAssert(scope1.info("1")?.match(#"\#(CategoryTag) ├ \#(InfoTag) \#(Location) 1"#) == true) - - scope2.enter() - XCTAssert(scope2.info("2")?.match(#"\#(CategoryTag) │ ├ \#(InfoTag) \#(Location) 2"#) == true) - - scope3.enter() - XCTAssert(scope3.info("3")?.match(#"\#(CategoryTag) │ │ ├ \#(InfoTag) \#(Location) 3"#) == true) - - scope1.leave() - XCTAssert(scope3.debug("3")?.match(#"\#(CategoryTag) │ ├ \#(DebugTag) \#(Location) 3"#) == true) - - scope2.leave() - XCTAssert(scope3.error("3")?.match(#"\#(CategoryTag) ├ \#(ErrorTag) \#(Location) 3"#) == true) - - scope3.leave() - XCTAssert(logger.fault("no scope")?.match(#"\#(CategoryTag) \#(FaultTag) \#(Location) no scope"#) == true) - } - - func test_scope_double_enter() { - let logger = DLog() - - let scope1 = logger.scope("My Scope") - - scope1.enter() - scope1.enter() - - XCTAssert(scope1.trace()?.match(#"\#(CategoryTag) ├ \#(TraceTag) \#(Location) \{func:\#(#function)"#) == true) - - scope1.leave() - scope1.leave() - - scope1.enter() - XCTAssert(scope1.trace()?.match(#"\#(CategoryTag) ├ \#(TraceTag) \#(Location) \{func:\#(#function)"#) == true) - scope1.leave() - - XCTAssert(logger.trace()?.match(#"\#(CategoryTag) \#(TraceTag) \#(Location) \{func:\#(#function)"#) == true) - } - - func test_scope_concurrent() { - let logger = DLog() - - wait(count: 10) { expectations in - for i in 1...10 { - DispatchQueue.global().async { - logger.scope("Scope \(i)") { - $0.debug("scope \(i)") - expectations[i-1].fulfill() - } - } - } - } - } - - func test_scope_duration() { - let logger = DLog() - - var scope = logger.scope("scope1") { _ in - delay() - } - XCTAssert(0.25 <= scope.duration) - - scope = logger.scope("scope2") - scope.enter() - delay() - scope.leave() - XCTAssert(0.25 <= scope.duration) - } + } + + func test_scope_duration() { + let logger = DLog() + + var scope = logger.scope("scope1") { _ in + delay() + } + XCTAssert(0.25 <= scope.duration) + + scope = logger.scope("scope2") + scope.enter() + delay() + scope.leave() + XCTAssert(0.25 <= scope.duration) + } func test_disabled_category() { let logger = DLog() @@ -1197,100 +1197,100 @@ final class ScopeTests: XCTestCase { } final class TraceTests: XCTestCase { - - func test_trace() { - let logger = DLog() - XCTAssert(logger.trace()?.match(#"\{func:test_trace\(\),thread:\{name:main,number:1\}\}$"#) == true) - } - - func test_trace_text() { - let logger = DLog() - XCTAssert(logger.trace("trace")?.match(#"\{func:test_trace_text\(\),thread:\{name:main,number:1\}\} trace$"#) == true) - } - - func test_trace_function() { - var config = LogConfig() - config.traceConfig.options = .function - let logger = DLog(config: config) - XCTAssert(logger.trace()?.match(#"\{func:test_trace_function\(\)\}"#) == true) - } - - func test_trace_queue_qos() { - var config = LogConfig() - config.traceConfig.options = [.queue, .thread] - config.traceConfig.threadConfig.options = .qos - let logger = DLog(config: config) - - - XCTAssert(logger.trace()?.match(#"\{queue:com.apple.main-thread"#) == true) - - let queues: [(String, DispatchQueue)] = [ - ("com.apple.root.background-qos", DispatchQueue.global(qos: .background)), - ("com.apple.root.utility-qos", DispatchQueue.global(qos: .utility)), - ("com.apple.root.default-qos", DispatchQueue.global(qos: .default)), - ("com.apple.root.user-initiated-qos", DispatchQueue.global(qos: .userInitiated)), - ("com.apple.root.user-interactive-qos", DispatchQueue.global(qos: .userInteractive)), - ("serial", DispatchQueue(label: "serial")), - ("concurrent", DispatchQueue(label: "concurrent", attributes: .concurrent)) - ] - wait(count: queues.count) { expectations in - for (i , (label, queue)) in queues.enumerated() { - queue.async { - XCTAssert(logger.trace()?.match(label) == true) - expectations[i].fulfill() - } - } - } - } - - func test_trace_thread_detach() { - var config = LogConfig() - config.traceConfig.options = .thread - let logger = DLog(config: config) - - wait { expectation in - Thread.detachNewThread { - XCTAssert(logger.trace()?.match(#"\{thread:\{number:\d+\}\}$"#) == true) - expectation.fulfill() - } + + func test_trace() { + let logger = DLog() + XCTAssert(logger.trace()?.match(#"\{func:test_trace\(\),thread:\{name:main,number:1\}\}$"#) == true) + } + + func test_trace_text() { + let logger = DLog() + XCTAssert(logger.trace("trace")?.match(#"\{func:test_trace_text\(\),thread:\{name:main,number:1\}\} trace$"#) == true) + } + + func test_trace_function() { + var config = LogConfig() + config.traceConfig.options = .function + let logger = DLog(config: config) + XCTAssert(logger.trace()?.match(#"\{func:test_trace_function\(\)\}"#) == true) + } + + func test_trace_queue_qos() { + var config = LogConfig() + config.traceConfig.options = [.queue, .thread] + config.traceConfig.threadConfig.options = .qos + let logger = DLog(config: config) + + + XCTAssert(logger.trace()?.match(#"\{queue:com.apple.main-thread"#) == true) + + let queues: [(String, DispatchQueue)] = [ + ("com.apple.root.background-qos", DispatchQueue.global(qos: .background)), + ("com.apple.root.utility-qos", DispatchQueue.global(qos: .utility)), + ("com.apple.root.default-qos", DispatchQueue.global(qos: .default)), + ("com.apple.root.user-initiated-qos", DispatchQueue.global(qos: .userInitiated)), + ("com.apple.root.user-interactive-qos", DispatchQueue.global(qos: .userInteractive)), + ("serial", DispatchQueue(label: "serial")), + ("concurrent", DispatchQueue(label: "concurrent", attributes: .concurrent)) + ] + wait(count: queues.count) { expectations in + for (i , (label, queue)) in queues.enumerated() { + queue.async { + XCTAssert(logger.trace()?.match(label) == true) + expectations[i].fulfill() } - } - - func test_trace_thread_all() { - var config = LogConfig() - config.traceConfig.options = .thread - config.traceConfig.threadConfig.options = .all - let logger = DLog(config: config) - XCTAssert(logger.trace()?.match(#"\{thread:\{name:main,number:1,priority:0\.\d,qos:[^,]+,stackSize:\d+ KB\}\}$"#) == true) - } - - func test_trace_thread_options_empty() { - var config = LogConfig() - config.traceConfig.options = .thread - config.traceConfig.threadConfig.options = [] - let logger = DLog(config: config) - XCTAssert(logger.trace()?.match(Empty) == true) - } - - func test_trace_stack() { - var config = LogConfig() - config.traceConfig.options = .stack - let logger = DLog(config: config) - let text = logger.trace() - XCTAssert(text?.match(#"\{stack:\[\{symbols:DLogTests\.TraceTests\.test_trace_stack\(\) -> \(\)\},"#) == true) - } - - func test_trace_stack_depth_all_pretty() { - var config = LogConfig() - config.traceConfig.options = .stack - config.traceConfig.stackConfig.options = .all - config.traceConfig.stackConfig.depth = 1 - config.traceConfig.style = .pretty - - let logger = DLog(config: config) - let text = logger.trace() - - let format = #""" + } + } + } + + func test_trace_thread_detach() { + var config = LogConfig() + config.traceConfig.options = .thread + let logger = DLog(config: config) + + wait { expectation in + Thread.detachNewThread { + XCTAssert(logger.trace()?.match(#"\{thread:\{number:\d+\}\}$"#) == true) + expectation.fulfill() + } + } + } + + func test_trace_thread_all() { + var config = LogConfig() + config.traceConfig.options = .thread + config.traceConfig.threadConfig.options = .all + let logger = DLog(config: config) + XCTAssert(logger.trace()?.match(#"\{thread:\{name:main,number:1,priority:0\.\d,qos:[^,]+,stackSize:\d+ KB\}\}$"#) == true) + } + + func test_trace_thread_options_empty() { + var config = LogConfig() + config.traceConfig.options = .thread + config.traceConfig.threadConfig.options = [] + let logger = DLog(config: config) + XCTAssert(logger.trace()?.match(Empty) == true) + } + + func test_trace_stack() { + var config = LogConfig() + config.traceConfig.options = .stack + let logger = DLog(config: config) + let text = logger.trace() + XCTAssert(text?.match(#"\{stack:\[\{symbols:DLogTests\.TraceTests\.test_trace_stack\(\) -> \(\)\},"#) == true) + } + + func test_trace_stack_depth_all_pretty() { + var config = LogConfig() + config.traceConfig.options = .stack + config.traceConfig.stackConfig.options = .all + config.traceConfig.stackConfig.depth = 1 + config.traceConfig.style = .pretty + + let logger = DLog(config: config) + let text = logger.trace() + + let format = #""" \{ stack : \[ \{ @@ -1303,21 +1303,21 @@ final class TraceTests: XCTestCase { \] \} """# - XCTAssert(text?.match(format) == true) - } - - func test_trace_config_empty() { - var config = LogConfig() - config.traceConfig.options = [] - let logger = DLog(config: config) - XCTAssert(logger.trace()?.match(Empty) == true) - } - - func test_trace_config_all() { - var config = LogConfig() - config.traceConfig.options = .all - let logger = DLog(config: config) - let text = logger.trace() - XCTAssert(text?.match(#"\#(Location) \{func:test_trace_config_all\(\),queue:com\.apple\.main-thread,stack:\[\{symbols:DLogTests\.TraceTests\.test_trace_config_all\(\) -> \(\)\}"#) == true) - } + XCTAssert(text?.match(format) == true) + } + + func test_trace_config_empty() { + var config = LogConfig() + config.traceConfig.options = [] + let logger = DLog(config: config) + XCTAssert(logger.trace()?.match(Empty) == true) + } + + func test_trace_config_all() { + var config = LogConfig() + config.traceConfig.options = .all + let logger = DLog(config: config) + let text = logger.trace() + XCTAssert(text?.match(#"\#(Location) \{func:test_trace_config_all\(\),queue:com\.apple\.main-thread,stack:\[\{symbols:DLogTests\.TraceTests\.test_trace_config_all\(\) -> \(\)\}"#) == true) + } } diff --git a/Tests/DLogTestsObjC/DLogTestsObjC.m b/Tests/DLogTestsObjC/DLogTestsObjC.m index 141d129..d7ca2ee 100644 --- a/Tests/DLogTestsObjC/DLogTestsObjC.m +++ b/Tests/DLogTestsObjC/DLogTestsObjC.m @@ -10,7 +10,7 @@ @implementation NSString (RegularExpression) - (BOOL)match:(NSString*)pattern { - return [self rangeOfString:pattern options:NSRegularExpressionSearch].location != NSNotFound; + return [self rangeOfString:pattern options:NSRegularExpressionSearch].location != NSNotFound; } @end @@ -18,11 +18,11 @@ - (BOOL)match:(NSString*)pattern { @implementation NSThread (Delay) + (void)sleep:(NSTimeInterval)ti { - [self sleepForTimeInterval: ti]; + [self sleepForTimeInterval: ti]; } + (void)sleep { - [self sleep: 0.25]; + [self sleep: 0.25]; } @end @@ -37,283 +37,283 @@ + (void)sleep { #define Location @" " static NSString* matchString(NSString* category, NSString* text) { - return [NSString stringWithFormat:@"%@" Padding LevelTag Location @"%@", (category ?: CategoryTag), text]; + return [NSString stringWithFormat:@"%@" Padding LevelTag Location @"%@", (category ?: CategoryTag), text]; } typedef void (^VoidBlock)(void); static NSString* readStream(int file, FILE* stream, VoidBlock block) { - __block NSMutableString* result = nil; - - let pipe = [NSPipe new]; - - let original = dup(file); - setvbuf(stream, nil, _IONBF, 0); - dup2(pipe.fileHandleForWriting.fileDescriptor, file); - - pipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle* handle) { - let text = [[NSString alloc] initWithData:handle.availableData encoding: NSUTF8StringEncoding]; - - if (result == nil) { - result = [NSMutableString new]; - } - if (text.length) { - [result appendString:text]; - } - }; - - block(); - - [NSThread sleep]; - - // Revert - fflush(stream); - dup2(original, file); - close(original); - - // Print - if (result != nil) { - printf("%s", result.UTF8String); + __block NSMutableString* result = nil; + + let pipe = [NSPipe new]; + + let original = dup(file); + setvbuf(stream, nil, _IONBF, 0); + dup2(pipe.fileHandleForWriting.fileDescriptor, file); + + pipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle* handle) { + let text = [[NSString alloc] initWithData:handle.availableData encoding: NSUTF8StringEncoding]; + + if (result == nil) { + result = [NSMutableString new]; } - - return result; + if (text.length) { + [result appendString:text]; + } + }; + + block(); + + [NSThread sleep]; + + // Revert + fflush(stream); + dup2(original, file); + close(original); + + // Print + if (result != nil) { + printf("%s", result.UTF8String); + } + + return result; } static NSString* read_stdout(VoidBlock block) { - return readStream(STDOUT_FILENO, stdout, block); + return readStream(STDOUT_FILENO, stdout, block); } static NSString* read_stderr(VoidBlock block) { - return readStream(STDERR_FILENO, stderr, block); + return readStream(STDERR_FILENO, stderr, block); } #pragma mark - Tests static void testAll(LogProtocol* logger, NSString *category) { - XCTAssertNotNil(logger); - - XCTAssertTrue([logger.log(@"log") match:matchString(category, @"log$")]); - XCTAssertTrue([logger.log(@"log %d", 123) match:matchString(category, @"log 123$")]); - XCTAssertTrue([logger.log(@"%@ %@", @"hello", @"world") match:matchString(category, @"hello world$")]); - - XCTAssertTrue([logger.trace() match:matchString(category, @"\\{func:testAll,thread:\\{name:main,number:1\\}\\}$")]); - XCTAssertTrue([logger.trace(@"trace") match:matchString(category, @"\\{func:testAll,thread:\\{name:main,number:1\\}\\} trace$")]); - XCTAssertTrue([logger.trace(@"trace%d", 1) match:matchString(category, @"\\{func:testAll,thread:\\{name:main,number:1\\}\\} trace1$")]); - - XCTAssertTrue([logger.debug(@"debug") match:matchString(category, @"debug$")]); - XCTAssertTrue([logger.info(@"info") match:matchString(category, @"info$")]); - XCTAssertTrue([logger.warning(@"warning") match:matchString(category, @"warning$")]); - XCTAssertTrue([logger.error(@"error") match:matchString(category, @"error$")]); - - XCTAssertNil(logger.assertion(YES)); - XCTAssertNil(logger.assertion(YES, @"assert$")); - XCTAssertNil(logger.assertion(YES, @"assert %d", 1)); - XCTAssertNotNil(logger.assertion(NO)); - XCTAssertTrue([logger.assertion(NO, @"assert") match:matchString(category, @"assert$")]); - XCTAssertTrue([logger.assertion(NO, @"assert%d", 1) match:matchString(category, @"assert1$")]); - - XCTAssertTrue([logger.fault(@"fault") match:matchString(category, @"fault$")]); - XCTAssertTrue([logger.fault(@"fault%d", 1) match:matchString(category, @"fault1$")]); + XCTAssertNotNil(logger); + + XCTAssertTrue([logger.log(@"log") match:matchString(category, @"log$")]); + XCTAssertTrue([logger.log(@"log %d", 123) match:matchString(category, @"log 123$")]); + XCTAssertTrue([logger.log(@"%@ %@", @"hello", @"world") match:matchString(category, @"hello world$")]); + + XCTAssertTrue([logger.trace() match:matchString(category, @"\\{func:testAll,thread:\\{name:main,number:1\\}\\}$")]); + XCTAssertTrue([logger.trace(@"trace") match:matchString(category, @"\\{func:testAll,thread:\\{name:main,number:1\\}\\} trace$")]); + XCTAssertTrue([logger.trace(@"trace%d", 1) match:matchString(category, @"\\{func:testAll,thread:\\{name:main,number:1\\}\\} trace1$")]); + + XCTAssertTrue([logger.debug(@"debug") match:matchString(category, @"debug$")]); + XCTAssertTrue([logger.info(@"info") match:matchString(category, @"info$")]); + XCTAssertTrue([logger.warning(@"warning") match:matchString(category, @"warning$")]); + XCTAssertTrue([logger.error(@"error") match:matchString(category, @"error$")]); + + XCTAssertNil(logger.assertion(YES)); + XCTAssertNil(logger.assertion(YES, @"assert$")); + XCTAssertNil(logger.assertion(YES, @"assert %d", 1)); + XCTAssertNotNil(logger.assertion(NO)); + XCTAssertTrue([logger.assertion(NO, @"assert") match:matchString(category, @"assert$")]); + XCTAssertTrue([logger.assertion(NO, @"assert%d", 1) match:matchString(category, @"assert1$")]); + + XCTAssertTrue([logger.fault(@"fault") match:matchString(category, @"fault$")]); + XCTAssertTrue([logger.fault(@"fault%d", 1) match:matchString(category, @"fault1$")]); } @interface DLogTestsObjC : XCTestCase @end - + @implementation DLogTestsObjC - (void)test_Log { - let logger = [DLog new]; - XCTAssertNotNil(logger); - - testAll(logger, nil); + let logger = [DLog new]; + XCTAssertNotNil(logger); + + testAll(logger, nil); } - (void)test_LogWithOutputs { - let logger = [[DLog alloc] initWithOutputs:@[]]; - XCTAssertNotNil(logger); - - XCTAssert([read_stdout(^{ logger.trace(); }) match: @"test_LogWithOutputs"]); + let logger = [[DLog alloc] initWithOutputs:@[]]; + XCTAssertNotNil(logger); + + XCTAssert([read_stdout(^{ logger.trace(); }) match: @"test_LogWithOutputs"]); } - (void)test_Category { - let logger = [DLog new]; - XCTAssertNotNil(logger); - - let net = logger[@"NET"]; - XCTAssertNotNil(net); - - testAll(net, @"\\[NET\\]"); + let logger = [DLog new]; + XCTAssertNotNil(logger); + + let net = logger[@"NET"]; + XCTAssertNotNil(net); + + testAll(net, @"\\[NET\\]"); } - (void)test_Emoji { - let logger = [[DLog alloc] initWithOutputs:@[LogOutput.textEmoji, LogOutput.stdOut]]; - XCTAssertNotNil(logger); - - XCTAssertTrue([logger.log(@"log") match:@"💬"]); - XCTAssertTrue([logger.trace() match:@"#️⃣"]); - XCTAssertTrue([logger.debug(@"debug") match:@"▶️"]); - XCTAssertTrue([logger.info(@"info") match:@"✅"]); - XCTAssertTrue([logger.warning(@"warning") match:@"⚠️"]); - XCTAssertTrue([logger.error(@"error") match:@"⚠️"]); - XCTAssertTrue([logger.assertion(NO) match:@"🅰️"]); - XCTAssertTrue([logger.fault(@"fault") match:@"🆘"]); + let logger = [[DLog alloc] initWithOutputs:@[LogOutput.textEmoji, LogOutput.stdOut]]; + XCTAssertNotNil(logger); + + XCTAssertTrue([logger.log(@"log") match:@"💬"]); + XCTAssertTrue([logger.trace() match:@"#️⃣"]); + XCTAssertTrue([logger.debug(@"debug") match:@"▶️"]); + XCTAssertTrue([logger.info(@"info") match:@"✅"]); + XCTAssertTrue([logger.warning(@"warning") match:@"⚠️"]); + XCTAssertTrue([logger.error(@"error") match:@"⚠️"]); + XCTAssertTrue([logger.assertion(NO) match:@"🅰️"]); + XCTAssertTrue([logger.fault(@"fault") match:@"🆘"]); } - (void)test_stdOutErr { - let logOut = [[DLog alloc] initWithOutputs:@[LogOutput.textPlain, LogOutput.stdOut]]; - XCTAssert([read_stdout(^{ logOut.trace(); }) match: @"test_stdOutErr"]); - - let logErr = [[DLog alloc] initWithOutputs:@[LogOutput.textPlain, LogOutput.stdErr]]; - XCTAssert([read_stderr(^{ logErr.trace(); }) match: @"test_stdOutErr"]); + let logOut = [[DLog alloc] initWithOutputs:@[LogOutput.textPlain, LogOutput.stdOut]]; + XCTAssert([read_stdout(^{ logOut.trace(); }) match: @"test_stdOutErr"]); + + let logErr = [[DLog alloc] initWithOutputs:@[LogOutput.textPlain, LogOutput.stdErr]]; + XCTAssert([read_stderr(^{ logErr.trace(); }) match: @"test_stdOutErr"]); } - (void)test_scope { - let logger = [DLog new]; - XCTAssertNotNil(logger); - - var scope = logger.scope(@"Scope 1", ^(LogScope* scope) { - testAll(scope, nil); - }); - XCTAssertNotNil(scope); - - scope = logger.scope(@"Scope 2"); - XCTAssertNotNil(scope); - - let text = read_stdout(^{ - [scope enter]; - testAll(scope, nil); - [scope leave]; - }); - XCTAssertTrue([text match:@"└ \\[Scope 2\\] \\(0\\.\\d+s\\)"]); + let logger = [DLog new]; + XCTAssertNotNil(logger); + + var scope = logger.scope(@"Scope 1", ^(LogScope* scope) { + testAll(scope, nil); + }); + XCTAssertNotNil(scope); + + scope = logger.scope(@"Scope 2"); + XCTAssertNotNil(scope); + + let text = read_stdout(^{ + [scope enter]; + testAll(scope, nil); + [scope leave]; + }); + XCTAssertTrue([text match:@"└ \\[Scope 2\\] \\(0\\.\\d+s\\)"]); } - + - (void)test_Interval { - let logger = [DLog new]; - XCTAssertNotNil(logger); - - let interval = logger.interval(@"interval", ^{ - [NSThread sleep]; - }); - - XCTAssertTrue(interval.duration >= 0.25); - - let text = read_stdout(^{ - [interval begin]; - [NSThread sleep]; - [interval end]; - }); - XCTAssertTrue([text match:@"\\{average:[0-9]+\\.[0-9]{3}s,duration:[0-9]+\\.[0-9]{3}s\\} interval$"]); + let logger = [DLog new]; + XCTAssertNotNil(logger); + + let interval = logger.interval(@"interval", ^{ + [NSThread sleep]; + }); + + XCTAssertTrue(interval.duration >= 0.25); + + let text = read_stdout(^{ + [interval begin]; + [NSThread sleep]; + [interval end]; + }); + XCTAssertTrue([text match:@"\\{average:[0-9]+\\.[0-9]{3}s,duration:[0-9]+\\.[0-9]{3}s\\} interval$"]); } - (void)test_AllOutputs { - let outputs = @[ - LogOutput.textPlain, - LogOutput.textEmoji, - LogOutput.textColored, - LogOutput.stdOut, - LogOutput.stdErr, - LogOutput.oslog, - [LogOutput oslog:@"com.dlog.objc"], - [LogOutput filterWithItem:^BOOL(LogItem* logItem) { - return logItem.type == LogTypeDebug; - }], - [LogOutput file:@"dlog.txt" append:NO], - [LogOutput net], - [LogOutput net:@"dlog"], - ]; - - for (LogOutput* output in outputs) { - let logger = [[DLog alloc] initWithOutputs:@[output]]; - XCTAssertNotNil(logger); - - logger.debug(@"debug"); - } + let outputs = @[ + LogOutput.textPlain, + LogOutput.textEmoji, + LogOutput.textColored, + LogOutput.stdOut, + LogOutput.stdErr, + LogOutput.oslog, + [LogOutput oslog:@"com.dlog.objc"], + [LogOutput filterWithItem:^BOOL(LogItem* logItem) { + return logItem.type == LogTypeDebug; + }], + [LogOutput file:@"dlog.txt" append:NO], + [LogOutput net], + [LogOutput net:@"dlog"], + ]; + + for (LogOutput* output in outputs) { + let logger = [[DLog alloc] initWithOutputs:@[output]]; + XCTAssertNotNil(logger); - [NSThread sleep]; + logger.debug(@"debug"); + } + + [NSThread sleep]; } - (void)test_filter { - let filterItem = [LogOutput filterWithItem:^BOOL(LogItem* logItem) { - return - [logItem.time compare:NSDate.now] == NSOrderedAscending && - [logItem.category isEqualToString:@"DLOG"] && - [logItem.scope.name isEqualToString:@"Scope"] && - logItem.type == LogTypeDebug && - [logItem.fileName isEqualToString:@"DLogTestsObjC.m"] && - [logItem.funcName isEqualToString:@"-[DLogTestsObjC test_filter]"] && - (logItem.line > __LINE__) && - [logItem.text isEqualToString:@"debug"]; - }]; - - let filterScope = [LogOutput filterWithScope:^BOOL(LogScope* scope) { - return [scope.name isEqualToString:@"Scope"]; - }]; - - XCTAssertNotNil(filterItem); - - let logger = [[DLog alloc] initWithOutputs:@[LogOutput.textPlain, filterItem, filterScope, LogOutput.stdOut]]; - XCTAssertNotNil(logger); - - let scope = logger.scope(@"Scope"); - XCTAssertNotNil(scope); - [scope enter]; - - XCTAssert([scope.debug(@"debug") match:matchString(nil, @"debug")]); - XCTAssertFalse([scope.log(@"log") match:matchString(nil, @"log")]); - - [scope leave]; + let filterItem = [LogOutput filterWithItem:^BOOL(LogItem* logItem) { + return + [logItem.time compare:NSDate.now] == NSOrderedAscending && + [logItem.category isEqualToString:@"DLOG"] && + [logItem.scope.name isEqualToString:@"Scope"] && + logItem.type == LogTypeDebug && + [logItem.fileName isEqualToString:@"DLogTestsObjC.m"] && + [logItem.funcName isEqualToString:@"-[DLogTestsObjC test_filter]"] && + (logItem.line > __LINE__) && + [logItem.text isEqualToString:@"debug"]; + }]; + + let filterScope = [LogOutput filterWithScope:^BOOL(LogScope* scope) { + return [scope.name isEqualToString:@"Scope"]; + }]; + + XCTAssertNotNil(filterItem); + + let logger = [[DLog alloc] initWithOutputs:@[LogOutput.textPlain, filterItem, filterScope, LogOutput.stdOut]]; + XCTAssertNotNil(logger); + + let scope = logger.scope(@"Scope"); + XCTAssertNotNil(scope); + [scope enter]; + + XCTAssert([scope.debug(@"debug") match:matchString(nil, @"debug")]); + XCTAssertFalse([scope.log(@"log") match:matchString(nil, @"log")]); + + [scope leave]; } - (void)test_Disabled { - let logger = DLog.disabled; - XCTAssertNotNil(logger); - - let text = read_stdout(^{ - logger.log(@"log"); - logger.trace(); - logger.debug(@"debug"); - logger.info(@"info"); - logger.warning(@"warning"); - logger.error(@"error"); - logger.assertion(NO); - logger.fault(@"fault"); - logger.scope(@"scope", ^(LogScope* scope) { - scope.error(@"error"); - }); - logger.interval(@"interval", ^{ [NSThread sleep]; }); + let logger = DLog.disabled; + XCTAssertNotNil(logger); + + let text = read_stdout(^{ + logger.log(@"log"); + logger.trace(); + logger.debug(@"debug"); + logger.info(@"info"); + logger.warning(@"warning"); + logger.error(@"error"); + logger.assertion(NO); + logger.fault(@"fault"); + logger.scope(@"scope", ^(LogScope* scope) { + scope.error(@"error"); }); - XCTAssertNil(text); + logger.interval(@"interval", ^{ [NSThread sleep]; }); + }); + XCTAssertNil(text); } - (void)test_metadata { - let logger = [DLog new]; - XCTAssertNotNil(logger); - - logger.metadata[@"id"] = @12345; - XCTAssert([logger.debug(@"debug") match:@"\\(id:12345\\)"]); - - logger.metadata[@"id"] = nil; - logger.metadata[@"name"] = @"Bob"; - XCTAssert([logger.debug(@"debug") match:@"\\(name:Bob\\)"]); - - // Category - let net = logger[@"NET"]; - XCTAssert([net.debug(@"debug") match:@"\\(name:Bob\\)"]); - [net.metadata clear]; - XCTAssert([net.debug(@"debug") match:@"\\(name:Bob\\)"] == NO); - - XCTAssert([logger.debug(@"debug") match:@"\\(name:Bob\\)"]); - - // Scope - var scope = logger.scope(@"Scope", ^(LogScope* scope) { - XCTAssert([scope.debug(@"debug") match:@"\\(name:Bob\\)"]); - scope.metadata[@"name"] = nil; - XCTAssert([scope.debug(@"debug") match:@"\\(name:Bob\\)"] == NO); - scope.metadata[@"id"] = @12345; - XCTAssert([scope.debug(@"debug") match:@"\\(id:12345\\)"]); - }); - - XCTAssert([logger.debug(@"debug") match:@"\\(name:Bob\\)"]); + let logger = [DLog new]; + XCTAssertNotNil(logger); + + logger.metadata[@"id"] = @12345; + XCTAssert([logger.debug(@"debug") match:@"\\(id:12345\\)"]); + + logger.metadata[@"id"] = nil; + logger.metadata[@"name"] = @"Bob"; + XCTAssert([logger.debug(@"debug") match:@"\\(name:Bob\\)"]); + + // Category + let net = logger[@"NET"]; + XCTAssert([net.debug(@"debug") match:@"\\(name:Bob\\)"]); + [net.metadata clear]; + XCTAssert([net.debug(@"debug") match:@"\\(name:Bob\\)"] == NO); + + XCTAssert([logger.debug(@"debug") match:@"\\(name:Bob\\)"]); + + // Scope + var scope = logger.scope(@"Scope", ^(LogScope* scope) { + XCTAssert([scope.debug(@"debug") match:@"\\(name:Bob\\)"]); + scope.metadata[@"name"] = nil; + XCTAssert([scope.debug(@"debug") match:@"\\(name:Bob\\)"] == NO); + scope.metadata[@"id"] = @12345; + XCTAssert([scope.debug(@"debug") match:@"\\(id:12345\\)"]); + }); + + XCTAssert([logger.debug(@"debug") match:@"\\(name:Bob\\)"]); } @end