Skip to content

Commit 87bfbd2

Browse files
committed
Add an article how to implement a log handler
# 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 81d3d07 commit 87bfbd2

File tree

2 files changed

+232
-13
lines changed

2 files changed

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