Skip to content

Commit cbd28c2

Browse files
authored
Add an article how to implement a log handler (#364)
# Motivation In my previous PR, I overhauled the README and the DocC catalog. While doing this I removed the guidance around how to implement a log handler. # Modifications This PR adds a DocC based guide containing the contents of the previous README guidance. # Result Guidance around log handler implementation in a modern DocC format.
1 parent c05d672 commit cbd28c2

File tree

2 files changed

+230
-13
lines changed

2 files changed

+230
-13
lines changed
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
# Implementing a log handler
2+
3+
Create a custom logging backend that provides logging services for your apps
4+
and libraries.
5+
6+
## Overview
7+
8+
To become a compatible logging backend that any `SwiftLog` consumer can use,
9+
you need to fulfill a few requirements, primarily conforming to the
10+
``LogHandler`` protocol.
11+
12+
### Implement with value type semantics
13+
14+
Your log handler **must be a `struct`** and exhibit value semantics. This
15+
ensures that changes to one logger don't affect others.
16+
17+
To verify that your handler reflects value semantics ensure that it passes this
18+
test:
19+
20+
```swift
21+
@Test
22+
func logHandlerValueSemantics() {
23+
LoggingSystem.bootstrap(MyLogHandler.init)
24+
var logger1 = Logger(label: "first logger")
25+
logger1.logLevel = .debug
26+
logger1[metadataKey: "only-on"] = "first"
27+
28+
var logger2 = logger1
29+
logger2.logLevel = .error // Must not affect logger1
30+
logger2[metadataKey: "only-on"] = "second" // Must not affect logger1
31+
32+
// These expectations must pass
33+
#expect(logger1.logLevel == .debug)
34+
#expect(logger2.logLevel == .error)
35+
#expect(logger1[metadataKey: "only-on"] == "first")
36+
#expect(logger2[metadataKey: "only-on"] == "second")
37+
}
38+
```
39+
40+
> Note: In special cases, it is acceptable for a log handler to provide
41+
> global log level overrides that may affect all log handlers created.
42+
43+
### Example implementation
44+
45+
Here's a complete example of a simple print-based log handler:
46+
47+
```swift
48+
import Foundation
49+
import Logging
50+
51+
public struct PrintLogHandler: LogHandler {
52+
private let label: String
53+
public var logLevel: Logger.Level = .info
54+
public var metadata: Logger.Metadata = [:]
55+
56+
public init(label: String) {
57+
self.label = label
58+
}
59+
60+
public func log(
61+
level: Logger.Level,
62+
message: Logger.Message,
63+
metadata: Logger.Metadata?,
64+
source: String,
65+
file: String,
66+
function: String,
67+
line: UInt
68+
) {
69+
let timestamp = ISO8601DateFormatter().string(from: Date())
70+
let levelString = level.rawValue.uppercased()
71+
72+
// Merge handler metadata with message metadata
73+
let combinedMetadata = Self.prepareMetadata(
74+
base: self.metadata
75+
explicit: metadata
76+
)
77+
78+
// Format metadata
79+
let metadataString = combinedMetadata.map { "\($0.key)=\($0.value)" }.joined(separator: ",")
80+
81+
// Create log line and print to console
82+
let logLine = "\(label) \(timestamp) \(levelString) [\(metadataString)]: \(message)"
83+
print(logLine)
84+
}
85+
86+
public subscript(metadataKey key: String) -> Logger.Metadata.Value? {
87+
get {
88+
return self.metadata[key]
89+
}
90+
set {
91+
self.metadata[key] = newValue
92+
}
93+
}
94+
95+
static func prepareMetadata(
96+
base: Logger.Metadata,
97+
explicit: Logger.Metadata?
98+
) -> Logger.Metadata? {
99+
var metadata = base
100+
101+
guard let explicit else {
102+
// all per-log-statement values are empty
103+
return metadata
104+
}
105+
106+
metadata.merge(explicit, uniquingKeysWith: { _, explicit in explicit })
107+
108+
return metadata
109+
}
110+
}
111+
112+
```
113+
114+
### Advanced features
115+
116+
#### Metadata providers
117+
118+
Metadata providers allow you to dynamically add contextual information to all
119+
log messages without explicitly passing it each time. Common use cases include
120+
request IDs, user sessions, or trace contexts that should be included in logs
121+
throughout a request's lifecycle.
122+
123+
```swift
124+
import Foundation
125+
import Logging
126+
127+
public struct PrintLogHandler: LogHandler {
128+
private let label: String
129+
public var logLevel: Logger.Level = .info
130+
public var metadata: Logger.Metadata = [:]
131+
public var metadataProvider: Logger.MetadataProvider?
132+
133+
public init(label: String) {
134+
self.label = label
135+
}
136+
137+
public func log(
138+
level: Logger.Level,
139+
message: Logger.Message,
140+
metadata: Logger.Metadata?,
141+
source: String,
142+
file: String,
143+
function: String,
144+
line: UInt
145+
) {
146+
let timestamp = ISO8601DateFormatter().string(from: Date())
147+
let levelString = level.rawValue.uppercased()
148+
149+
// Get provider metadata
150+
let providerMetadata = metadataProvider?.get() ?? [:]
151+
152+
// Merge handler metadata with message metadata
153+
let combinedMetadata = Self.prepareMetadata(
154+
base: self.metadata,
155+
provider: self.metadataProvider,
156+
explicit: metadata
157+
)
158+
159+
// Format metadata
160+
let metadataString = combinedMetadata.map { "\($0.key)=\($0.value)" }.joined(separator: ",")
161+
162+
// Create log line and print to console
163+
let logLine = "\(label) \(timestamp) \(levelString) [\(metadataString)]: \(message)"
164+
print(logLine)
165+
}
166+
167+
public subscript(metadataKey key: String) -> Logger.Metadata.Value? {
168+
get {
169+
return self.metadata[key]
170+
}
171+
set {
172+
self.metadata[key] = newValue
173+
}
174+
}
175+
176+
static func prepareMetadata(
177+
base: Logger.Metadata,
178+
provider: Logger.MetadataProvider?,
179+
explicit: Logger.Metadata?
180+
) -> Logger.Metadata? {
181+
var metadata = base
182+
183+
let provided = provider?.get() ?? [:]
184+
185+
guard !provided.isEmpty || !((explicit ?? [:]).isEmpty) else {
186+
// all per-log-statement values are empty
187+
return metadata
188+
}
189+
190+
if !provided.isEmpty {
191+
metadata.merge(provided, uniquingKeysWith: { _, provided in provided })
192+
}
193+
194+
if let explicit = explicit, !explicit.isEmpty {
195+
metadata.merge(explicit, uniquingKeysWith: { _, explicit in explicit })
196+
}
197+
198+
return metadata
199+
}
200+
}
201+
```
202+
203+
### Performance considerations
204+
205+
1. **Avoid blocking**: Don't block the calling thread for I/O operations.
206+
2. **Lazy evaluation**: Remember that messages and metadata are autoclosures.
207+
3. **Memory efficiency**: Don't hold onto large amounts of messages.
208+
209+
## See Also
210+
211+
- ``LogHandler``
212+
- ``StreamLogHandler``
213+
- ``MultiplexLogHandler``

Sources/Logging/LogHandler.swift

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,23 @@
3131
/// When developing your `LogHandler`, please make sure the following test works.
3232
///
3333
/// ```swift
34-
/// LoggingSystem.bootstrap(MyLogHandler.init) // your LogHandler might have a different bootstrapping step
35-
/// var logger1 = Logger(label: "first logger")
36-
/// logger1.logLevel = .debug
37-
/// logger1[metadataKey: "only-on"] = "first"
38-
///
39-
/// var logger2 = logger1
40-
/// logger2.logLevel = .error // this must not override `logger1`'s log level
41-
/// logger2[metadataKey: "only-on"] = "second" // this must not override `logger1`'s metadata
42-
///
43-
/// XCTAssertEqual(.debug, logger1.logLevel)
44-
/// XCTAssertEqual(.error, logger2.logLevel)
45-
/// XCTAssertEqual("first", logger1[metadataKey: "only-on"])
46-
/// XCTAssertEqual("second", logger2[metadataKey: "only-on"])
34+
/// @Test
35+
/// func logHandlerValueSemantics() {
36+
/// LoggingSystem.bootstrap(MyLogHandler.init)
37+
/// var logger1 = Logger(label: "first logger")
38+
/// logger1.logLevel = .debug
39+
/// logger1[metadataKey: "only-on"] = "first"
40+
///
41+
/// var logger2 = logger1
42+
/// logger2.logLevel = .error // Must not affect logger1
43+
/// logger2[metadataKey: "only-on"] = "second" // Must not affect logger1
44+
///
45+
/// // These expectations must pass
46+
/// #expect(logger1.logLevel == .debug)
47+
/// #expect(logger2.logLevel == .error)
48+
/// #expect(logger1[metadataKey: "only-on"] == "first")
49+
/// #expect(logger2[metadataKey: "only-on"] == "second")
50+
/// }
4751
/// ```
4852
///
4953
/// ### Special cases

0 commit comments

Comments
 (0)