diff --git a/Sources/DLog/Log.swift b/Sources/DLog/Log.swift index 81ebc22..f25ea5e 100644 --- a/Sources/DLog/Log.swift +++ b/Sources/DLog/Log.swift @@ -248,12 +248,12 @@ public class Log: NSObject { /// - Returns: An `LogScope` object for the new scope. /// @discardableResult - public func scope(_ name: String, metadata: Metadata? = nil, fileID: String = #fileID, file: String = #file, function: String = #function, line: UInt = #line, closure: ((LogScope) -> Void)? = nil) -> LogScope? { + public func scope(_ name: String, fileID: String = #fileID, file: String = #file, function: String = #function, line: UInt = #line, closure: ((LogScope) -> Void)? = nil) -> LogScope? { guard logger.output != nil else { return nil } let location = LogLocation(fileID: fileID, file: file, function: function, line: line) - let scope = LogScope(name: name, logger: logger, category: category, config: config, metadata: metadata, location: location) + let scope = LogScope(name: name, logger: logger, category: category, config: config, metadata: metadata.data, location: location) if let closure { scope.enter(fileID: fileID, file: file, function: function, line: line) closure(scope) diff --git a/Sources/DLog/LogData.swift b/Sources/DLog/LogData.swift new file mode 100644 index 0000000..e48fab3 --- /dev/null +++ b/Sources/DLog/LogData.swift @@ -0,0 +1,70 @@ +// LogData.swift +// +// Created by Iurii Khvorost on 2025/01/04. +// Copyright © 2025 Iurii Khvorost. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +typealias LogData = [String : Any] + +extension LogData { + + static func data(from items: [(Option, String, Any)], options: Option) -> LogData { + let keyValues: [(String, Any)] = items + .compactMap { (option: Option, key: String, value: Any) in + // Option + assert(option is Option.Element) + guard options.contains(option as! Option.Element) else { + return nil + } + + // Key + assert(key.isEmpty == false) + + // Value + if let text = value as? String, text.isEmpty { + return nil + } + else if let dict = value as? LogData, dict.isEmpty { + return nil + } + + return (key, value) + } + return Dictionary(uniqueKeysWithValues: keyValues) + } +} + +extension Dictionary { + + func json(pretty: Bool = false) -> String { + guard count > 0 else { + return "" + } + 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 "" + } + return json.replacingOccurrences(of: "\"", with: "") + } +} diff --git a/Sources/DLog/LogInterval.swift b/Sources/DLog/LogInterval.swift index e85bb1d..c05be93 100644 --- a/Sources/DLog/LogInterval.swift +++ b/Sources/DLog/LogInterval.swift @@ -135,7 +135,7 @@ public class LogInterval { return text.replacingOccurrences(of: "[INTERVAL]", with: "[INTERVAL:\(message)]") } - override func data() -> Metadata? { + override func data() -> LogData? { let items: [(IntervalOptions, String, Any)] = [ (.duration, "duration", stringFromTimeInterval(duration)), (.count, "count", stats.count), @@ -144,7 +144,7 @@ public class LogInterval { (.max, "max", stringFromTimeInterval(stats.max)), (.average, "average", stringFromTimeInterval(stats.average)), ] - return Metadata.metadata(from: items, options: config.intervalConfig.options) + return LogData.data(from: items, options: config.intervalConfig.options) } override func messageText() -> String { diff --git a/Sources/DLog/LogItem.swift b/Sources/DLog/LogItem.swift index c1ac8cf..2639ee9 100644 --- a/Sources/DLog/LogItem.swift +++ b/Sources/DLog/LogItem.swift @@ -260,7 +260,7 @@ extension Log { } } - func data() -> Metadata? { + func data() -> LogData? { nil } @@ -274,11 +274,11 @@ extension Log { public var description: String { var sign = "\(config.sign)" - var time = Log.Item.dateFormatter.string(from: self.time) - var level = String(format: "[%02d]", self.stack?.count ?? 0) - var category = "[\(self.category)]" - var location = "<\(self.location.fileName):\(self.location.line)>" - var metadata = self.metadata.json() + var time = Log.Item.dateFormatter.string(from: time) + var level = String(format: "[%02d]", stack?.count ?? 0) + var category = "[\(category)]" + var location = "<\(location.fileName):\(location.line)>" + var metadata = metadata.json() var data = data()?.json() ?? "" switch config.style { diff --git a/Sources/DLog/LogMetadata.swift b/Sources/DLog/LogMetadata.swift index d468b90..146533b 100644 --- a/Sources/DLog/LogMetadata.swift +++ b/Sources/DLog/LogMetadata.swift @@ -23,57 +23,16 @@ import Foundation -/// Metadata dictionary type -// TODO: LogData -public typealias Metadata = [String : Any] -extension Metadata { - - static func metadata(from items: [(Option, String, Any)], options: Option) -> Metadata { - let keyValues: [(String, Any)] = items - .compactMap { (option: Option, key: String, value: Any) in - // Option - assert(option is Option.Element) - guard options.contains(option as! Option.Element) else { - return nil - } - - // Key - assert(key.isEmpty == false) - - // Value - if let text = value as? String, text.isEmpty { - return nil - } - else if let dict = value as? Metadata, dict.isEmpty { - return nil - } - - return (key, value) - } - return Dictionary(uniqueKeysWithValues: keyValues) - } - - func json(pretty: Bool = false) -> String { - guard count > 0 else { - return "" - } - 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 "" - } - return json.replacingOccurrences(of: "\"", with: "") - } -} +public typealias Metadata = [String : Codable] + /// Contextual metadata @objcMembers public class LogMetadata: NSObject { - private var _data = Metadata() + private var _data: Metadata - init(data: Metadata = Metadata()) { + init(data: Metadata) { _data = data } @@ -82,7 +41,7 @@ public class LogMetadata: NSObject { } /// Gets and sets a value by a string key to metadata. - public subscript(name: String) -> Any? { + public subscript(name: String) -> Codable? { get { synchronized(self) { _data[name] } } diff --git a/Sources/DLog/LogScope.swift b/Sources/DLog/LogScope.swift index 8f5d77f..fd53943 100644 --- a/Sources/DLog/LogScope.swift +++ b/Sources/DLog/LogScope.swift @@ -77,7 +77,7 @@ public class LogScope: Log { return text.replacingOccurrences(of: "[SCOPE]", with: "[SCOPE:\(message)]") } - override func data() -> Metadata? { + override func data() -> LogData? { duration > 0 ? ["duration": stringFromTimeInterval(duration)] : nil } @@ -102,10 +102,10 @@ public class LogScope: Log { } } - init(name: String, logger: DLog, category: String, config: LogConfig, metadata: Metadata?, location: LogLocation) { + init(name: String, logger: DLog, category: String, config: LogConfig, metadata: Metadata, location: LogLocation) { self.name = name self.location = location - super.init(logger: logger, category: category, config: config, metadata: metadata ?? logger.metadata.data) + super.init(logger: logger, category: category, config: config, metadata: metadata) } private func item(type: LogType, stack: [Bool]) -> Item { diff --git a/Sources/DLog/LogTrace.swift b/Sources/DLog/LogTrace.swift index 8c73225..dae3478 100644 --- a/Sources/DLog/LogTrace.swift +++ b/Sources/DLog/LogTrace.swift @@ -47,7 +47,7 @@ public class LogTrace: Log.Item { super.init(time: Date(), category: category, stack: stack, type: .trace, location: location, metadata: metadata, message: message, config: config) } - override func data() -> Metadata? { + override func data() -> LogData? { let items: [(TraceOptions, String, Any)] = [ (.function, "func", funcInfo(function: location.function, config: config.traceConfig.funcConfig)), (.process, "process", processMetadata(processInfo: traceInfo.processInfo, config: config.traceConfig.processConfig)), @@ -55,6 +55,6 @@ public class LogTrace: Log.Item { (.stack, "stack", stackMetadata(moduleName: location.moduleName, stackAddresses: traceInfo.stackAddresses, config: config.traceConfig.stackConfig)), (.thread, "thread", threadMetadata(thread: traceInfo.thread, tid: traceInfo.tid, config: config.traceConfig.threadConfig)), ] - return Metadata.metadata(from: items, options: config.traceConfig.options) + return LogData.data(from: items, options: config.traceConfig.options) } } diff --git a/Sources/DLog/TraceProcess.swift b/Sources/DLog/TraceProcess.swift index e307bf0..c0b7b1b 100644 --- a/Sources/DLog/TraceProcess.swift +++ b/Sources/DLog/TraceProcess.swift @@ -91,17 +91,17 @@ public struct ProcessConfig { public var wakeupsOptions: WakeupsOptions = .all } -func wakeupsMetadata(options: WakeupsOptions) -> Metadata { +func wakeupsMetadata(options: WakeupsOptions) -> LogData { let power = TaskInfo.power let items: [(WakeupsOptions, String, Any)] = [ (.interrupt, "interrupt", power.task_interrupt_wakeups), (.idle, "idle", power.task_platform_idle_wakeups), (.timer, "timer", power.task_timer_wakeups_bin_1 + power.task_timer_wakeups_bin_2) ] - return Metadata.metadata(from: items, options: options) + return LogData.data(from: items, options: options) } -func processMetadata(processInfo: ProcessInfo, config: ProcessConfig) -> Metadata { +func processMetadata(processInfo: ProcessInfo, config: ProcessConfig) -> LogData { let items: [(ProcessOptions, String, Any)] = [ (.cpu, "cpu", "\(threadsInfo().cpuUsage)%"), (.guid, "guid", processInfo.globallyUniqueString), @@ -111,5 +111,5 @@ func processMetadata(processInfo: ProcessInfo, config: ProcessConfig) -> Metadat (.threads, "threads", threadsInfo().threadsCount), (.wakeups, "wakeups", wakeupsMetadata(options: config.wakeupsOptions)), ] - return Metadata.metadata(from: items, options: config.options) + return LogData.data(from: items, options: config.options) } diff --git a/Sources/DLog/TraceStack.swift b/Sources/DLog/TraceStack.swift index acab414..f4baacc 100644 --- a/Sources/DLog/TraceStack.swift +++ b/Sources/DLog/TraceStack.swift @@ -81,7 +81,7 @@ fileprivate func swift_demangle(_ mangled: String) -> String? { return String(cString: cString) } -func stackMetadata(moduleName: String, stackAddresses: ArraySlice, config: StackConfig) -> [Metadata] { +func stackMetadata(moduleName: String, stackAddresses: ArraySlice, config: StackConfig) -> [LogData] { stackAddresses .compactMap { item -> (address: UInt, module: String, offset: UInt, symbol: String)? in let address = item.uintValue @@ -117,6 +117,6 @@ func stackMetadata(moduleName: String, stackAddresses: ArraySlice, con (.offset, "offset", item.element.offset), (.symbol, "symbol", swift_demangle(item.element.symbol) ?? item.element.symbol), ] - return Metadata.metadata(from: items, options: config.options) + return LogData.data(from: items, options: config.options) } } diff --git a/Sources/DLog/TraceThread.swift b/Sources/DLog/TraceThread.swift index a09d46a..2f5c4c4 100644 --- a/Sources/DLog/TraceThread.swift +++ b/Sources/DLog/TraceThread.swift @@ -102,7 +102,7 @@ public struct ThreadConfig { public var options: ThreadOptions = .compact } -func threadMetadata(thread: Thread, tid: UInt64, config: ThreadConfig) -> Metadata { +func threadMetadata(thread: Thread, tid: UInt64, config: ThreadConfig) -> LogData { let items: [(ThreadOptions, String, Any)] = [ (.name, "name", thread.info.name), (.number, "number", thread.info.number), @@ -111,5 +111,5 @@ func threadMetadata(thread: Thread, tid: UInt64, config: ThreadConfig) -> Metada (.stackSize, "stackSize", "\(ByteCountFormatter.string(fromByteCount: Int64(thread.stackSize), countStyle: .memory))"), (.tid, "tid", tid), ] - return Metadata.metadata(from: items, options: config.options) + return LogData.data(from: items, options: config.options) } diff --git a/Tests/DLogTests/DLogTests.swift b/Tests/DLogTests/DLogTests.swift index 967ade8..a154216 100644 --- a/Tests/DLogTests/DLogTests.swift +++ b/Tests/DLogTests/DLogTests.swift @@ -127,8 +127,8 @@ let Empty = ">$" final class DLogTests: XCTestCase { func test_trace() { - let log = DLog(metadata: ["a": 100]) - let item = log.trace() + let log = DLog() + let item = log.trace("trace") XCTAssert(item?.traceInfo.queueLabel == "com.apple.main-thread") }