Skip to content

Commit 90244e3

Browse files
committed
Refactor Logging implementation
Motivation: The original implementation of Logging was done in more haste than should have been, without proper attention given to the semantic requirements. As the Swift ecosystem has matured a bit, lessons have been learned on handling metadata and passing of external context into internal subcomponents. A mixture of the "protocol-based context passing" and "explicit context passing" patterns have been adopted. Both patterns are more fully described in the Swift forum discussion: https://forums.swift.org/t/the-context-passing-problem/39162 Modifications: - Add: `RedisLogging` namespace with references to static keys and labels that are used for Logging throughout the library - Add: `Logger` static computed properties to access the Logger prototypes used in connection and connection pools - Add: `RedisClientWithUserContext` protocol and `UserContextRedisClient` types to assist with wrapping client types for custom logger contexts - Remove: `logger` property from `RedisClient` requirements - Change: Many log statements to have higher or lower log levels for their appropriate context - Change: `RedisConnection` and `RedisConnectionPool` to conform to `RedisClientWithUserContext` - Change: `logging(to:)` protocol requirement to return a `RedisClient` existential - Change: ConnectionPool to explicitly pass a logger instance around for pooling methods Result: Logging in RediStack will now have a stronger contract of where and how logs will be generated and which context will be used. Fixes #79 and #74
1 parent 1cf438e commit 90244e3

File tree

12 files changed

+540
-250
lines changed

12 files changed

+540
-250
lines changed

CONTRIBUTING.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ All contributors are expected to adhere to this project's [Code of Conduct](CODE
99

1010
## Development
1111

12+
### Project Vision
13+
14+
For clarification behind certain API design decisions, or to get up to speed on the vision of the project, check out the [API Design](./docs/api-design) pages.
15+
1216
### File Copyright Headers
1317

1418
When adding a new file to the project, add the following heading to the top of the file:

Sources/RediStack/ChannelHandlers/RedisCommandHandler.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,6 @@ extension RedisCommandHandler: ChannelInboundHandler {
109109
RedisMetrics.commandSuccessCount.increment()
110110
}
111111
}
112-
113-
// public func channelUnregistered(context: ChannelHandlerContext) {
114-
// self._drainCommandQueue(because: RedisClientError.connectionClosed)
115-
// }
116-
117112
}
118113

119114
// MARK: ChannelOutboundHandler

Sources/RediStack/Connection Pool/ConnectionPool.swift

Lines changed: 80 additions & 71 deletions
Large diffs are not rendered by default.

Sources/RediStack/RedisClient.swift

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the RediStack open source project
44
//
5-
// Copyright (c) 2019 RediStack project authors
5+
// Copyright (c) 2019-2020 RediStack project authors
66
// Licensed under Apache License v2.0
77
//
88
// See LICENSE.txt for license information
@@ -16,6 +16,8 @@ import protocol Foundation.LocalizedError
1616
import struct Logging.Logger
1717
import NIO
1818

19+
// - Important: Any RedisClient defined by RediStack should conform to the RedisClientWithUserContext protocol as well
20+
1921
/// An object capable of sending commands and receiving responses.
2022
///
2123
/// let client = ...
@@ -26,9 +28,6 @@ import NIO
2628
public protocol RedisClient {
2729
/// The `NIO.EventLoop` that this client operates on.
2830
var eventLoop: EventLoop { get }
29-
30-
/// The `Logging.Logger` that this client uses.
31-
var logger: Logger { get }
3231

3332
/// Sends the desired command with the specified arguments.
3433
/// - Parameters:
@@ -37,9 +36,10 @@ public protocol RedisClient {
3736
/// - Returns: A `NIO.EventLoopFuture` that will resolve with the Redis command response.
3837
func send(command: String, with arguments: [RESPValue]) -> EventLoopFuture<RESPValue>
3938

40-
/// Updates the client's logger.
41-
/// - Parameter logger: The `Logging.Logger` that is desired to receive all client logs.
42-
func setLogging(to logger: Logger)
39+
/// Temporarily overrides the default logger for command logs to the provided instance.
40+
/// - Parameter logger: The `Logging.Logger` instance to use for command logs.
41+
/// - Returns: A RedisClient with the temporary override for command logging.
42+
func logging(to logger: Logger) -> RedisClient
4343
}
4444

4545
extension RedisClient {
@@ -51,16 +51,7 @@ extension RedisClient {
5151
}
5252
}
5353

54-
extension RedisClient {
55-
/// Updates the client's logger and returns a reference to itself for chaining method calls.
56-
/// - Parameter logger: The `Logging.Logger` that is desired to receive all client logs.
57-
/// - Returns: A reference to the client for chaining method calls.
58-
@inlinable
59-
public func logging(to logger: Logger) -> Self {
60-
self.setLogging(to: logger)
61-
return self
62-
}
63-
}
54+
// MARK: Errors
6455

6556
/// When working with `RedisClient`, runtime errors can be thrown to indicate problems with connection state, decoding assertions, or otherwise.
6657
public struct RedisClientError: LocalizedError, Equatable, Hashable {
@@ -84,8 +75,8 @@ public struct RedisClientError: LocalizedError, Equatable, Hashable {
8475
public var errorDescription: String? {
8576
let message: String
8677
switch self.baseError {
87-
case .connectionClosed: message = "Connection was closed while trying to send command."
88-
case let .failedRESPConversion(type): message = "Failed to convert RESP to \(type)"
78+
case .connectionClosed: message = "trying to send command with a closed connection"
79+
case let .failedRESPConversion(type): message = "failed to convert RESP to \(type)"
8980
case let .assertionFailure(text): message = text
9081
}
9182
return "(RediStack) \(message)"

Sources/RediStack/RedisConnection.swift

Lines changed: 73 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the RediStack open source project
44
//
5-
// Copyright (c) 2019 RediStack project authors
5+
// Copyright (c) 2019-2020 RediStack project authors
66
// Licensed under Apache License v2.0
77
//
88
// See LICENSE.txt for license information
@@ -60,13 +60,13 @@ extension RedisConnection {
6060
to socket: SocketAddress,
6161
on eventLoop: EventLoop,
6262
password: String? = nil,
63-
logger: Logger = .init(label: "RediStack.RedisConnection"),
63+
logger: Logger = .redisBaseConnectionLogger,
6464
tcpClient: ClientBootstrap? = nil
6565
) -> EventLoopFuture<RedisConnection> {
6666
let client = tcpClient ?? ClientBootstrap.makeRedisTCPClient(group: eventLoop)
6767

6868
return client.connect(to: socket)
69-
.map { return RedisConnection(configuredRESPChannel: $0, logger: logger) }
69+
.map { return RedisConnection(configuredRESPChannel: $0, context: logger) }
7070
.flatMap { connection in
7171
guard let pw = password else {
7272
return connection.eventLoop.makeSucceededFuture(connection)
@@ -95,15 +95,10 @@ extension RedisConnection {
9595
///
9696
/// Note: `wait()` is used in the example for simplicity. Never call `wait()` on an event loop.
9797
///
98-
/// See `NIO.SocketAddress`, `NIO.EventLoop`, and `RedisClient`
99-
public final class RedisConnection: RedisClient {
98+
/// See `NIO.SocketAddress`, `NIO.EventLoop`, and `RedisClient`.
99+
public final class RedisConnection: RedisClient, RedisClientWithUserContext {
100100
/// A unique identifer to represent this connection.
101101
public let id = UUID()
102-
public private(set) var logger: Logger {
103-
didSet {
104-
self.logger[metadataKey: String(describing: RedisConnection.self)] = .string(self.id.description)
105-
}
106-
}
107102
public var eventLoop: EventLoop { return self.channel.eventLoop }
108103
/// Is the connection to Redis still open?
109104
public var isConnected: Bool {
@@ -126,6 +121,8 @@ public final class RedisConnection: RedisClient {
126121
}
127122

128123
internal let channel: Channel
124+
private let systemContext: Context
125+
private var logger: Logger { self.systemContext }
129126

130127
private let autoflush: NIOAtomic<Bool> = .makeAtomic(value: true)
131128
private let _stateLock = Lock()
@@ -138,13 +135,18 @@ public final class RedisConnection: RedisClient {
138135
deinit {
139136
if isConnected {
140137
assertionFailure("close() was not called before deinit!")
141-
logger.warning("RedisConnection did not properly shutdown before deinit!")
138+
self.logger.warning("connection was not properly shutdown before deinit")
142139
}
143140
}
144141

145-
internal init(configuredRESPChannel: Channel, logger: Logger) {
142+
internal init(configuredRESPChannel: Channel, context: Context) {
146143
self.channel = configuredRESPChannel
147-
self.logger = logger
144+
// there is a mix of verbiage here as the API is forward thinking towards "baggage context"
145+
// while right now it's just an alias of a 'Logging.logger'
146+
// in the future this will probably be a property _on_ the context
147+
var logger = context
148+
logger[metadataKey: RedisLogging.MetadataKeys.connectionID] = "\(self.id.description)"
149+
self.systemContext = logger
148150

149151
RedisMetrics.activeConnectionCount.increment()
150152
RedisMetrics.totalConnectionCount.increment()
@@ -157,11 +159,11 @@ public final class RedisConnection: RedisClient {
157159
guard self.state == .open else { return }
158160

159161
self.state = .closed
160-
self.logger.error("Channel was closed unexpectedly.")
162+
self.logger.error("connection was closed unexpectedly")
161163
RedisMetrics.activeConnectionCount.decrement()
162164
}
163165

164-
self.logger.trace("Connection created.")
166+
self.logger.trace("connection created")
165167
}
166168

167169
internal enum ConnectionState {
@@ -177,18 +179,35 @@ extension RedisConnection {
177179
/// Sends the command with the provided arguments to Redis.
178180
///
179181
/// See `RedisClient.send(command:with:)`.
180-
///
181182
/// - Note: The timing of when commands are actually sent to Redis can be controlled with the `RedisConnection.sendCommandsImmediately` property.
182183
/// - Returns: A `NIO.EventLoopFuture` that resolves with the command's result stored in a `RESPValue`.
183184
/// If a `RedisError` is returned, the future will be failed instead.
184185
public func send(command: String, with arguments: [RESPValue]) -> EventLoopFuture<RESPValue> {
185-
self.logger.trace("Received command")
186+
self.eventLoop.flatSubmit {
187+
return self.send(command: command, with: arguments, context: nil)
188+
}
189+
}
190+
191+
internal func send(
192+
command: String,
193+
with arguments: [RESPValue],
194+
context: Context?
195+
) -> EventLoopFuture<RESPValue> {
196+
self.eventLoop.preconditionInEventLoop()
197+
198+
let logger = self.prepareLoggerForUse(context)
186199

187200
guard self.isConnected else {
188201
let error = RedisClientError.connectionClosed
189202
logger.warning("\(error.localizedDescription)")
190203
return self.channel.eventLoop.makeFailedFuture(error)
191204
}
205+
logger.trace("received command request")
206+
207+
logger.debug("sending command", metadata: [
208+
RedisLogging.MetadataKeys.commandKeyword: "\(command)",
209+
RedisLogging.MetadataKeys.commandArguments: "\(arguments)"
210+
])
192211

193212
var message: [RESPValue] = [.init(bulk: command)]
194213
message.append(contentsOf: arguments)
@@ -204,17 +223,21 @@ extension RedisConnection {
204223
let duration = DispatchTime.now().uptimeNanoseconds - startTime
205224
RedisMetrics.commandRoundTripTime.recordNanoseconds(duration)
206225

207-
// log the error here instead
208-
guard case let .failure(error) = result else {
209-
self.logger.trace("Command completed.")
210-
return
226+
// log data based on the result
227+
switch result {
228+
case let .failure(error):
229+
logger.error("command failed", metadata: [
230+
RedisLogging.MetadataKeys.error: "\(error.localizedDescription)"
231+
])
232+
233+
case let .success(value):
234+
logger.debug("command succeeded", metadata: [
235+
RedisLogging.MetadataKeys.commandResult: "\(value)"
236+
])
211237
}
212-
self.logger.error("\(error.localizedDescription)")
213238
}
214239

215-
self.logger.debug("Sending command \"\(command)\"\(arguments.count > 0 ? " with \(arguments)" : "")")
216-
217-
defer { self.logger.trace("Command sent through channel.") }
240+
defer { logger.trace("command sent") }
218241

219242
if self.sendCommandsImmediately {
220243
return channel.writeAndFlush(command).flatMap { promise.futureResult }
@@ -232,26 +255,34 @@ extension RedisConnection {
232255
/// See [https://redis.io/commands/quit](https://redis.io/commands/quit)
233256
/// - Important: Regardless if the returned `NIO.EventLoopFuture` fails or succeeds - after calling this method the connection should no longer be
234257
/// used for sending commands to Redis.
258+
/// - Parameter logger: An optional logger instance to use while trying to close the connection.
259+
/// If one is not provided, the pool will use its default logger.
235260
/// - Returns: A `NIO.EventLoopFuture` that resolves when the connection has been closed.
236261
@discardableResult
237-
public func close() -> EventLoopFuture<Void> {
238-
self.logger.trace("Received request to close the connection.")
262+
public func close(logger: Logger? = nil) -> EventLoopFuture<Void> {
263+
let logger = self.prepareLoggerForUse(logger)
239264

240265
guard self.isConnected else {
241266
// return the channel's close future, which is resolved as the last step in channel shutdown
267+
logger.info("received duplicate request to close connection")
242268
return self.channel.closeFuture
243269
}
270+
logger.trace("received request to close the connection")
244271

245272
// we're now in a shutdown state, starting with the command queue.
246273
self.state = .shuttingDown
247274

248-
let notification = self.sendQuitCommand() // send "QUIT" so that all the responses are written out
275+
let notification = self.sendQuitCommand(logger: logger) // send "QUIT" so that all the responses are written out
249276
.flatMap { self.closeChannel() } // close the channel from our end
250277

251-
notification.whenFailure { self.logger.error("Encountered error during close(): \($0)") }
278+
notification.whenFailure {
279+
logger.error("error while closing connection", metadata: [
280+
RedisLogging.MetadataKeys.error: "\($0)"
281+
])
282+
}
252283
notification.whenSuccess {
253284
self.state = .closed
254-
self.logger.debug("Connection closed.")
285+
logger.trace("connection is now closed")
255286
RedisMetrics.activeConnectionCount.decrement()
256287
}
257288

@@ -260,19 +291,19 @@ extension RedisConnection {
260291

261292
/// Bypasses everything for a normal command and explicitly just sends a "QUIT" command to Redis.
262293
/// - Note: If the command fails, the `NIO.EventLoopFuture` will still succeed - as it's not critical for the command to succeed.
263-
private func sendQuitCommand() -> EventLoopFuture<Void> {
294+
private func sendQuitCommand(logger: Logger) -> EventLoopFuture<Void> {
264295
let promise = channel.eventLoop.makePromise(of: RESPValue.self)
265296
let command = RedisCommand(
266297
message: .array([RESPValue(bulk: "QUIT")]),
267298
responsePromise: promise
268299
)
269300

270-
logger.trace("Sending QUIT command.")
301+
logger.trace("sending QUIT command")
271302

272303
return channel.writeAndFlush(command) // write the command
273304
.flatMap { promise.futureResult } // chain the callback to the response's
274-
.map { _ in () } // ignore the result's value
275-
.recover { _ in () } // if there's an error, just return to void
305+
.map { _ in logger.trace("sent QUIT command") } // ignore the result's value
306+
.recover { _ in logger.debug("recovered from error sending QUIT") } // if there's an error, just return to void
276307
}
277308

278309
/// Attempts to close the `NIO.Channel`.
@@ -303,11 +334,13 @@ extension RedisConnection {
303334
// MARK: Logging
304335

305336
extension RedisConnection {
306-
/// Updates the client's logger, attaching this connection's ID to the metadata.
307-
///
308-
/// See `RedisClient.setLogging(to:)` and `RedisConnection.id`.
309-
/// - Parameter logger: The `Logging.Logger` that is desired to receive all client logs.
310-
public func setLogging(to logger: Logger) {
311-
self.logger = logger
337+
public func logging(to logger: Logger) -> RedisClient {
338+
return UserContextRedisClient(client: self, context: self.prepareLoggerForUse(logger))
339+
}
340+
341+
private func prepareLoggerForUse(_ logger: Logger?) -> Logger {
342+
guard var logger = logger else { return self.logger }
343+
logger[metadataKey: RedisLogging.MetadataKeys.connectionID] = "\(self.id)"
344+
return logger
312345
}
313346
}

0 commit comments

Comments
 (0)