From d22556d04e89703da67d754e70253949a58d7b09 Mon Sep 17 00:00:00 2001 From: "T. R. Bernstein" Date: Mon, 2 Mar 2026 18:53:17 +0100 Subject: [PATCH] Output metadata as JSON Metadata shall be machine readable (see SwiftLog/Structured Logging) and the previous metadata serialization erased the nesting structure. The JSON output ensures that the metadata can be quickly analyzed by either a copy and paste into a JSON editor or a small CLI pipeline. See https://swiftpackageindex.com/apple/swift-log/1.10.1/documentation/logging/002-structuredlogging for more information on metadata. --- .../Logger Metadata Extension.swift | 41 +++++++++++++++++++ Sources/Logging OSLog/Logging OSLog.swift | 7 +--- Tests/Logging OSLog Tests/Logging Test.swift | 14 ++++++- 3 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 Sources/Logging OSLog/Logger Metadata Extension.swift diff --git a/Sources/Logging OSLog/Logger Metadata Extension.swift b/Sources/Logging OSLog/Logger Metadata Extension.swift new file mode 100644 index 0000000..5dd3304 --- /dev/null +++ b/Sources/Logging OSLog/Logger Metadata Extension.swift @@ -0,0 +1,41 @@ +import Logging + +extension Logging.Logger.Metadata { + public func asJSON() -> String { + return Self.asJSON(self) + } + + private static func asJSON(_ metadata: Logging.Logger.Metadata) -> String { + var outputParts: [String: String] = [:] + for (key, value) in metadata { + let jsonKey = Self.escapeForJSON(key) + outputParts[jsonKey] = Self.asJSON(value) + } + return "{" + outputParts.map { "\"\($0)\": \($1)" }.joined(separator: ", ") + "}" + } + + private static func asJSON(_ metadata: [Logging.Logger.MetadataValue]) -> String { + var outputParts: [String] = [] + for item in metadata { + outputParts.append(Self.asJSON(item)) + } + return "[" + outputParts.joined(separator: ", ") + "]" + } + + private static func asJSON(_ metadata: Logging.Logger.MetadataValue) -> String { + switch metadata { + case .dictionary(let subvalues): + return Self.asJSON(subvalues) + case .array(let subvalues): + return Self.asJSON(subvalues) + case .string(let subvalue): + return "\"" + Self.escapeForJSON(subvalue) + "\"" + case .stringConvertible(let subvalue): + return "\"" + Self.escapeForJSON("\(subvalue)") + "\"" + } + } + + private static func escapeForJSON(_ data: String) -> String { + return data.replacingOccurrences(of: "\"", with: "\\\"") + } +} diff --git a/Sources/Logging OSLog/Logging OSLog.swift b/Sources/Logging OSLog/Logging OSLog.swift index a8197d1..bfd965e 100644 --- a/Sources/Logging OSLog/Logging OSLog.swift +++ b/Sources/Logging OSLog/Logging OSLog.swift @@ -103,11 +103,6 @@ public struct LoggingOSLog: LogHandler { guard let metadata = metadata else { continue } metadataAggregator.merge(metadata) { return $1 } } - return Self.joinedMetadata(metadataAggregator) - } - - private static func joinedMetadata(_ metadata: Logging.Logger.Metadata, with separator: String = ", ") -> String? { - guard !metadata.isEmpty else { return nil } - return metadata.map { "\"\($0)\": \"\($1)\"" }.joined(separator: separator) + return metadataAggregator.asJSON() } } diff --git a/Tests/Logging OSLog Tests/Logging Test.swift b/Tests/Logging OSLog Tests/Logging Test.swift index e475144..173fbc4 100644 --- a/Tests/Logging OSLog Tests/Logging Test.swift +++ b/Tests/Logging OSLog Tests/Logging Test.swift @@ -13,4 +13,16 @@ import LoggingOSLog @Test func canLogMessage() { let logger = Logging.Logger(label: "de.astzweig.loggingoslog.Test") logger.info("Test message") -} \ No newline at end of file +} + +@Test func canLogMessageWithMetadata() { + let logger = Logging.Logger(label: "de.astzweig.loggingoslog.Test") + logger.info("Test message", metadata: [ + "request.id": "20140801", + "request.dirname": "\"Impossible\"", + "request.authorization": [ + "bearer": "empty" + ], + "request.values": ["1", "rootID", ["key": "value"]] + ]) +}