Skip to content

Commit 576af05

Browse files
committed
[PoC] A LogHandler with a live-configurable log level
1 parent 4a50528 commit 576af05

File tree

2 files changed

+178
-0
lines changed

2 files changed

+178
-0
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#if LoggingSupport && ReloadingSupport
2+
3+
public import Logging
4+
public import ServiceLifecycle
5+
import Synchronization
6+
7+
/// A log handler whose log level can be controlled the hot-reloaded configuration value `logLevel`.
8+
///
9+
/// ## Usage
10+
///
11+
/// ```swift
12+
/// // An existing log handler, for example `StreamLogHandler.standardError(...)`.
13+
/// let logHandler = ...
14+
/// // A config reader with at least one reloading provider, containing a value for key `logLevel`.
15+
/// let configReader = ...
16+
/// let configurableLogHandler = LiveConfigurableLogHandler(
17+
/// upstream: logHandler,
18+
/// config: configReader,
19+
/// diagnosticLogger: Logger(label: "LiveConfigurableLogHandler", factory: { _ in logHandler })
20+
/// )
21+
///
22+
/// // 1. Add `configurableLogHandler` to a ServiceGroup.
23+
/// // 2. Bootstrap `configurableLogHandler` as the Swift Log backend.
24+
/// ```
25+
public struct LiveConfigurableLogHandler<Upstream: LogHandler> {
26+
var upstream: Upstream
27+
var service: LiveConfigurableLogHandlerService
28+
29+
init(
30+
upstream: Upstream,
31+
service: LiveConfigurableLogHandlerService
32+
) {
33+
self.upstream = upstream
34+
self.service = service
35+
}
36+
37+
public init(upstream: Upstream, config: ConfigReader, diagnosticLogger: Logger) {
38+
self.init(
39+
upstream: upstream,
40+
service: .init(
41+
config: config,
42+
diagnosticLogger: diagnosticLogger
43+
)
44+
)
45+
}
46+
}
47+
48+
extension LiveConfigurableLogHandler: Service {
49+
public func run() async throws {
50+
try await service.run()
51+
}
52+
}
53+
54+
extension LiveConfigurableLogHandler: LogHandler {
55+
public var logLevel: Logger.Level {
56+
get {
57+
service.currentLogLevel ?? upstream.logLevel
58+
}
59+
set {
60+
upstream.logLevel = newValue
61+
}
62+
}
63+
64+
public var metadata: Logger.Metadata {
65+
get { upstream.metadata }
66+
set { upstream.metadata = newValue }
67+
}
68+
69+
public subscript(metadataKey key: String) -> Logging.Logger.Metadata.Value? {
70+
get {
71+
upstream[metadataKey: key]
72+
}
73+
set(newValue) {
74+
upstream[metadataKey: key] = newValue
75+
}
76+
}
77+
78+
public func log(
79+
level: Logger.Level,
80+
message: Logger.Message,
81+
metadata: Logger.Metadata?,
82+
source: String,
83+
file: String,
84+
function: String,
85+
line: UInt
86+
) {
87+
guard level >= logLevel else {
88+
return
89+
}
90+
upstream.log(
91+
level: level,
92+
message: message,
93+
metadata: metadata,
94+
source: source,
95+
file: file,
96+
function: function,
97+
line: line
98+
)
99+
}
100+
}
101+
102+
extension ConfigKey {
103+
fileprivate static let logLevel: Self = ["logLevel"]
104+
}
105+
106+
final class LiveConfigurableLogHandlerService: Sendable {
107+
108+
private let config: ConfigReader
109+
private let diagnosticLogger: Logger
110+
111+
private struct Storage {
112+
var logLevel: Logger.Level?
113+
}
114+
private let storage: Mutex<Storage>
115+
116+
var currentLogLevel: Logger.Level? {
117+
storage.withLock { $0.logLevel }
118+
}
119+
120+
init(
121+
config: ConfigReader,
122+
diagnosticLogger: Logger
123+
) {
124+
self.config = config
125+
self.diagnosticLogger = diagnosticLogger
126+
self.storage = .init(.init(logLevel: config.string(forKey: .logLevel)))
127+
}
128+
}
129+
130+
extension LiveConfigurableLogHandlerService {
131+
func run() async throws {
132+
diagnosticLogger.debug("Starting")
133+
defer {
134+
diagnosticLogger.debug("Stopping")
135+
}
136+
try await config.watchString(forKey: .logLevel, as: Logger.Level.self) { updates in
137+
for await logLevel in updates {
138+
let oldLogLevel = storage.withLock { storage in
139+
let oldLogLevel = storage.logLevel
140+
storage.logLevel = logLevel
141+
return oldLogLevel
142+
}
143+
diagnosticLogger.debug(
144+
"Updated log level",
145+
metadata: [
146+
"newLogLevelOverride": "\(logLevel?.rawValue ?? "<nil>")",
147+
"oldLogLevelOverride": "\(oldLogLevel?.rawValue ?? "<nil>")",
148+
]
149+
)
150+
}
151+
}
152+
}
153+
}
154+
155+
#endif
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#if LoggingSupport && ReloadingSupport
2+
3+
import Configuration
4+
import Testing
5+
import Logging
6+
7+
// TODO: Flesh out this test
8+
// struct LiveConfigurableLogHandlerTests {
9+
// @Test
10+
// func reload() async throws {
11+
12+
// let handler = LiveConfigurableLogHandler(
13+
// upstream:
14+
// )
15+
16+
// }
17+
// }
18+
19+
// struct TestLogHandler: LogHandler {
20+
21+
// }
22+
23+
#endif

0 commit comments

Comments
 (0)