Skip to content

Commit 0db4e18

Browse files
committed
Make JSON encoders and decoders configurable
1 parent dfbb2fb commit 0db4e18

File tree

2 files changed

+66
-8
lines changed

2 files changed

+66
-8
lines changed

Sources/JSONLogger.swift

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import Logging
2727

2828
This configuration is interesting mostly to generate `json-seq` stream.
2929
To do this, set the inter-JSON separator to `[]`, the prefix to `[0x1e]` and the suffix to `[0x0a]`,
30-
or use the convenience ``JSONLogger/forJSONSeq()``.
30+
or use the convenience ``forJSONSeq(on:label:metadataProvider:)``.
3131

3232
Finally, another interesting configuration is to set the separator to `[0xff]` or `[0xfe]`.
3333
These bytes should not appear in valid UTF-8 strings and should be able to be used to separate JSON payloads.
@@ -37,6 +37,37 @@ import Logging
3737
The output file descriptor is also customizable and is `stdout` by default. */
3838
public struct JSONLogger : LogHandler {
3939

40+
public static let defaultJSONEncoder: JSONEncoder = {
41+
let res = JSONEncoder()
42+
res.outputFormatting = [.withoutEscapingSlashes]
43+
res.keyEncodingStrategy = .useDefaultKeys
44+
res.dateEncodingStrategy = .iso8601
45+
res.dataEncodingStrategy = .base64
46+
res.nonConformingFloatEncodingStrategy = .convertToString(positiveInfinity: "+inf", negativeInfinity: "-inf", nan: "nan")
47+
return res
48+
}()
49+
50+
public static let defaultJSONCodersForStringConvertibles: (JSONEncoder, JSONDecoder) = {
51+
let encoder = JSONEncoder()
52+
encoder.outputFormatting = [.withoutEscapingSlashes]
53+
encoder.keyEncodingStrategy = .useDefaultKeys
54+
encoder.dateEncodingStrategy = .iso8601
55+
encoder.dataEncodingStrategy = .base64
56+
encoder.nonConformingFloatEncodingStrategy = .throw
57+
let decoder = JSONDecoder()
58+
if #available(macOS 12.0, tvOS 15.0, iOS 15.0, watchOS 8.0, *) {
59+
decoder.allowsJSON5 = false
60+
}
61+
decoder.keyDecodingStrategy = .useDefaultKeys
62+
decoder.dateDecodingStrategy = .iso8601
63+
decoder.dataDecodingStrategy = .base64
64+
if #available(macOS 12.0, tvOS 15.0, iOS 15.0, watchOS 8.0, *) {
65+
decoder.assumesTopLevelDictionary = false
66+
}
67+
decoder.nonConformingFloatDecodingStrategy = .throw
68+
return (encoder, decoder)
69+
}()
70+
4071
public var logLevel: Logger.Level = .info
4172

4273
public var metadata: Logger.Metadata = [:] {
@@ -52,21 +83,28 @@ public struct JSONLogger : LogHandler {
5283
public let suffix: Data
5384

5485
/**
55-
If `true`, the `Encodable` properties in the metadata will be encoded and kept structured in the resulting log line.
56-
If the encoding fails or this property is set to `false` the String value will be used. */
57-
public var tryEncodingStringConvertibles: Bool
86+
If non-`nil`, the `Encodable` stringConvertible properties in the metadata will be encoded as `JSON` using the `JSONEncoder` and `JSONDecoder`.
87+
If the encoding fails or this property is set to `nil` the String value will be used. */
88+
public var jsonCodersForStringConvertibles: (JSONEncoder, JSONDecoder)?
5889

5990
public static func forJSONSeq(on fd: FileDescriptor = .standardError, label: String, metadataProvider: Logger.MetadataProvider? = LoggingSystem.metadataProvider) -> Self {
6091
return Self(label: label, fd: fd, lineSeparator: Data(), prefix: Data([0x1e]), suffix: Data([0x0a]), metadataProvider: metadataProvider)
6192
}
6293

63-
public init(label: String, fd: FileDescriptor = .standardError, lineSeparator: Data = Data("\n".utf8), prefix: Data = Data(), suffix: Data = Data(), tryEncodingStringConvertibles: Bool = true, metadataProvider: Logger.MetadataProvider? = LoggingSystem.metadataProvider) {
94+
public init(
95+
label: String,
96+
fd: FileDescriptor = .standardError,
97+
lineSeparator: Data = Data("\n".utf8), prefix: Data = Data(), suffix: Data = Data(),
98+
jsonEncoder: JSONEncoder = Self.defaultJSONEncoder,
99+
jsonCodersForStringConvertibles: (JSONEncoder, JSONDecoder) = Self.defaultJSONCodersForStringConvertibles,
100+
metadataProvider: Logger.MetadataProvider? = LoggingSystem.metadataProvider
101+
) {
64102
self.label = label
65103
self.outputFileDescriptor = fd
66104
self.lineSeparator = lineSeparator
67105
self.prefix = prefix
68106
self.suffix = suffix
69-
self.tryEncodingStringConvertibles = tryEncodingStringConvertibles
107+
self.jsonCodersForStringConvertibles = jsonCodersForStringConvertibles
70108

71109
self.metadataProvider = metadataProvider
72110
}
@@ -164,8 +202,12 @@ extension JSONLogger {
164202
case let .array(array): .array (array .map (jsonMetadataValue(_:)))
165203
case let .dictionary(dictionary): .object(dictionary.mapValues(jsonMetadataValue(_:)))
166204
case let .stringConvertible(s):
167-
if tryEncodingStringConvertibles, let c = s as? any Encodable, let encoded = try? JSON(encodable: c) {
168-
encoded
205+
if let (encoder, decoder) = jsonCodersForStringConvertibles,
206+
let c = s as? any Encodable,
207+
let data = try? encoder.encode(c),
208+
let json = try? decoder.decode(JSON.self, from: data)
209+
{
210+
json
169211
} else {
170212
.string(s.description)
171213
}

Tests/JSONLoggerTests.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,20 @@ final class JSONLoggerTests: XCTestCase {
5555
XCTAssertEqual(data.first, 0x0a)
5656
}
5757

58+
func testEncodeMetadataAsJSON() throws {
59+
struct BestStruct : Encodable, CustomStringConvertible {
60+
var val: Int
61+
var description: String {"manually: \(val)"}
62+
}
63+
let ref = LogLine(level: .info, message: "Not first log message", metadata: .object(["yolo": .object(["val": .number(21)])]), label: "best-logger", source: "dummy-source", file: "dummy-file", function: "dummy-function", line: 42)
64+
65+
let pipe = Pipe()
66+
let jsonLogger = JSONLogger(label: "best-logger", fd: FileDescriptor(rawValue: pipe.fileHandleForWriting.fileDescriptor))
67+
jsonLogger.log(level: ref.level, message: "\(ref.message)", metadata: ["yolo": .stringConvertible(BestStruct(val: 21))], source: ref.source, file: ref.file, function: ref.function, line: ref.line)
68+
try pipe.fileHandleForWriting.close()
69+
let data = try pipe.fileHandleForReading.readToEnd() ?? Data()
70+
let line = try JSONDecoder().decode(LogLine.self, from: data)
71+
XCTAssertEqual(line, ref)
72+
}
73+
5874
}

0 commit comments

Comments
 (0)