Skip to content

Commit 209ba87

Browse files
committed
Revisit user Logging configuration for connections and clients
Motivation: Logging is more dynamic in real world usage than the current static heavy API allows. Users generally want to be capable of updating connection logger metadata to attach dynamic properties such as an HTTP request ID for log tracing. Modifications: - Move all logs to `RedisConnection` - Add `id: UUID` property to `RedisConnection` - Add `logging` property and `setLogging(to:)` method requirements to `RedisClient` - Add chainable `logging(to:)` method extension to `RedisClient` - Add additional `trace` log statements to `RedisConnection` - Change when `RedisConnection.init` logging and metric calls are made - Change some `debug` log statements to `trace in `RedisConnection` Result: Users should have infinitely more flexibility in how RedisConnection, and RedisClient implementations in general, behave in regards to logging.
1 parent e0ebf92 commit 209ba87

File tree

5 files changed

+125
-60
lines changed

5 files changed

+125
-60
lines changed

Sources/RediStack/ChannelHandlers/RedisCommandHandler.swift

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import struct Foundation.UUID
16-
import Logging
1715
import NIO
1816

1917
/// The `NIO.ChannelOutboundHandler.OutboundIn` type for `RedisCommandHandler`.
@@ -39,24 +37,18 @@ public struct RedisCommand {
3937
public final class RedisCommandHandler {
4038
/// FIFO queue of promises waiting to receive a response value from a sent command.
4139
private var commandResponseQueue: CircularBuffer<EventLoopPromise<RESPValue>>
42-
private var logger: Logger
4340

4441
deinit {
45-
guard self.commandResponseQueue.count > 0 else { return }
46-
self.logger[metadataKey: "Queue Size"] = "\(self.commandResponseQueue.count)"
47-
self.logger.warning("Command handler deinit when queue is not empty")
42+
if !self.commandResponseQueue.isEmpty {
43+
assertionFailure("Command handler deinit when queue is not empty! Queue size: \(self.commandResponseQueue.count)")
44+
}
4845
}
4946

50-
/// - Parameters:
51-
/// - initialQueueCapacity: The initial queue size to start with. The default is `3`. `RedisCommandHandler` stores all
47+
/// - Parameter initialQueueCapacity: The initial queue size to start with. The default is `3`. `RedisCommandHandler` stores all
5248
/// `RedisCommand.responsePromise` objects into a buffer, and unless you intend to execute several concurrent commands against Redis,
5349
/// and don't want the buffer to resize, you shouldn't need to set this parameter.
54-
/// - logger: The `Logging.Logger` instance to use.
55-
/// The logger will have a `Foundation.UUID` value attached as metadata to uniquely identify this instance.
56-
public init(initialQueueCapacity: Int = 3, logger: Logger = Logger(label: "RediStack.CommandHandler")) {
50+
public init(initialQueueCapacity: Int = 3) {
5751
self.commandResponseQueue = CircularBuffer(initialCapacity: initialQueueCapacity)
58-
self.logger = logger
59-
self.logger[metadataKey: "CommandHandler"] = "\(UUID())"
6052
}
6153
}
6254

@@ -78,8 +70,6 @@ extension RedisCommandHandler: ChannelInboundHandler {
7870

7971
self.commandResponseQueue.removeAll()
8072
queue.forEach { $0.fail(error) }
81-
82-
self.logger.critical("Error in channel pipeline.", metadata: ["error": "\(error.localizedDescription)"])
8373

8474
context.close(promise: nil)
8575
}
@@ -92,10 +82,7 @@ extension RedisCommandHandler: ChannelInboundHandler {
9282
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
9383
let value = self.unwrapInboundIn(data)
9484

95-
guard let leadPromise = self.commandResponseQueue.popFirst() else {
96-
self.logger.critical("Read triggered with no promise waiting in the queue!")
97-
return
98-
}
85+
guard let leadPromise = self.commandResponseQueue.popFirst() else { return }
9986

10087
switch value {
10188
case .error(let e):

Sources/RediStack/ChannelHandlers/RedisMessageEncoder.swift

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,37 +12,34 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import struct Logging.Logger
1615
import NIO
1716

18-
/// Encodes outgoing `RESPValue` data into a raw `ByteBuffer`
19-
/// according to the Redis Serialization Protocol (RESP).
17+
#if DEBUG
18+
// used only for debugging purposes where we build a formatted string for the encoded bytes
19+
private func getPrintableString(for buffer: inout ByteBuffer) -> String {
20+
return String(describing: buffer.getString(at: 0, length: buffer.readableBytes))
21+
.dropFirst(9)
22+
.dropLast()
23+
.description
24+
}
25+
#endif
26+
27+
/// Encodes outgoing `RESPValue` data into a raw `ByteBuffer` according to the Redis Serialization Protocol (RESP).
2028
///
2129
/// See `NIO.MessageToByteEncoder`, `RESPTranslator`, and [https://redis.io/topics/protocol](https://redis.io/topics/protocol)
2230
public final class RedisMessageEncoder: MessageToByteEncoder {
2331
/// See `MessageToByteEncoder.OutboundIn`
2432
public typealias OutboundIn = RESPValue
2533

26-
private let logger: Logger
27-
28-
public init(logger: Logger = Logger(label: "RediStack.RedisMessageEncoder")) {
29-
self.logger = logger
30-
}
34+
public init() { }
3135

3236
/// Encodes the `RedisValue` to bytes, following the RESP specification.
3337
///
3438
/// See [https://redis.io/topics/protocol](https://redis.io/topics/protocol) and `RESPEncoder.encode(data:out:)`
3539
public func encode(data: RESPValue, out: inout ByteBuffer) throws {
3640
out.writeRESPValue(data)
37-
38-
logger.debug("Encoded \(data) to \(getPrintableString(for: &out))")
39-
}
40-
41-
// used only for debugging purposes where we build a formatted string for the encoded bytes
42-
private func getPrintableString(for buffer: inout ByteBuffer) -> String {
43-
return String(describing: buffer.getString(at: 0, length: buffer.readableBytes))
44-
.dropFirst(9)
45-
.dropLast()
46-
.description
41+
// if you're looking to debug the value, set a breakpoint on the return and use `getPrintableString(for:)`
42+
// e.g. `po getPrintableString(for: &out)`
43+
return
4744
}
4845
}

Sources/RediStack/RedisClient.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import struct Logging.Logger
1516
import NIO
1617

1718
/// An object capable of sending commands and receiving responses.
@@ -24,13 +25,20 @@ import NIO
2425
public protocol RedisClient {
2526
/// The `EventLoop` that this client operates on.
2627
var eventLoop: EventLoop { get }
28+
29+
/// The `Logging.Logger` that this client uses.
30+
var logger: Logger { get }
2731

2832
/// Sends the desired command with the specified arguments.
2933
/// - Parameters:
3034
/// - command: The command to execute.
3135
/// - arguments: The arguments, if any, to be sent with the command.
3236
/// - Returns: An `EventLoopFuture` that will resolve with the Redis command response.
3337
func send(command: String, with arguments: [RESPValue]) -> EventLoopFuture<RESPValue>
38+
39+
/// Updates the client's logger.
40+
/// - Parameter logger: The `Logging.Logger` that is desired to receive all client logs.
41+
func setLogging(to logger: Logger)
3442
}
3543

3644
extension RedisClient {
@@ -41,3 +49,14 @@ extension RedisClient {
4149
return self.send(command: command, with: [])
4250
}
4351
}
52+
53+
extension RedisClient {
54+
/// Updates the client's logger and returns a reference to itself for chaining method calls.
55+
/// - Parameter logger: The `Logging.Logger` that is desired to receive all client logs.
56+
/// - Returns: A reference to the client for chaining method calls.
57+
@inlinable
58+
public func logging(to logger: Logger) -> Self {
59+
self.setLogging(to: logger)
60+
return self
61+
}
62+
}

Sources/RediStack/RedisConnection.swift

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ import Metrics
1919
import NIO
2020
import NIOConcurrencyHelpers
2121

22-
private let loggingKeyID = String(describing: RedisConnection.self)
23-
2422
extension RedisConnection {
2523
/// The documented default port that Redis connects through.
2624
///
@@ -54,15 +52,15 @@ extension RedisConnection {
5452
/// - socket: The `NIO.SocketAddress` information of the Redis instance to connect to.
5553
/// - eventLoop: The `NIO.EventLoop` that this connection will execute all tasks on.
5654
/// - password: The optional password to use for authorizing the connection with Redis.
57-
/// - logger: The `Logging.Logger` instance to use for all logging purposes. If one is not provided, one will be created.
55+
/// - logger: The `Logging.Logger` instance to use for all client logging purposes. If one is not provided, one will be created.
5856
/// A `Foundation.UUID` will be attached to the metadata to uniquely identify this connection instance's logs.
59-
/// - tcpClient: If you have chosen to configure a `NIO.ClientBootstrap` yourself, this will.
57+
/// - tcpClient: If you have chosen to configure a `NIO.ClientBootstrap` yourself, this will be used instead of the `makeRedisTCPClient` instance.
6058
/// - Returns: A `NIO.EventLoopFuture` that resolves with the new connection after it has been opened, and if a `password` is provided, authenticated.
6159
public static func connect(
6260
to socket: SocketAddress,
6361
on eventLoop: EventLoop,
6462
password: String? = nil,
65-
logger: Logger = Logger(label: "RediStack.RedisConnection"),
63+
logger: Logger = .init(label: "RediStack.RedisConnection"),
6664
tcpClient: ClientBootstrap? = nil
6765
) -> EventLoopFuture<RedisConnection> {
6866
let client = tcpClient ?? ClientBootstrap.makeRedisTCPClient(group: eventLoop)
@@ -101,7 +99,13 @@ extension RedisConnection {
10199
///
102100
/// See `NIO.SocketAddress`, `NIO.EventLoop`, and `RedisClient`
103101
public final class RedisConnection: RedisClient {
104-
/// See `RedisClient.eventLoop`
102+
/// A unique identifer to represent this connection.
103+
public let id = UUID()
104+
public private(set) var logger: Logger {
105+
didSet {
106+
self.logger[metadataKey: String(describing: RedisConnection.self)] = .string(self.id.description)
107+
}
108+
}
105109
public var eventLoop: EventLoop { return self.channel.eventLoop }
106110
/// Is the connection to Redis still open?
107111
public var isConnected: Bool {
@@ -124,11 +128,10 @@ public final class RedisConnection: RedisClient {
124128
}
125129

126130
internal let channel: Channel
127-
private var logger: Logger
128131

129132
private let autoflush = Atomic<Bool>(value: true)
130133
private let _stateLock = Lock()
131-
private var _state: ConnectionState
134+
private var _state = ConnectionState.open
132135
private var state: ConnectionState {
133136
get { return _stateLock.withLock { self._state } }
134137
set(newValue) { _stateLock.withLockVoid { self._state = newValue } }
@@ -144,10 +147,7 @@ public final class RedisConnection: RedisClient {
144147
internal init(configuredRESPChannel: Channel, logger: Logger) {
145148
self.channel = configuredRESPChannel
146149
self.logger = logger
147-
148-
self.logger[metadataKey: loggingKeyID] = "\(UUID())"
149-
self._state = .open
150-
self.logger.debug("Connection created.")
150+
151151
RedisMetrics.activeConnectionCount.increment()
152152
RedisMetrics.totalConnectionCount.increment()
153153

@@ -159,9 +159,11 @@ public final class RedisConnection: RedisClient {
159159
guard self.state == .open else { return }
160160

161161
self.state = .closed
162-
self.logger.warning("Channel was closed unexpectedly.")
162+
self.logger.error("Channel was closed unexpectedly.")
163163
RedisMetrics.activeConnectionCount.decrement()
164164
}
165+
166+
self.logger.trace("Connection created.")
165167
}
166168

167169
internal enum ConnectionState {
@@ -182,6 +184,8 @@ extension RedisConnection {
182184
/// - Returns: A `NIO.EventLoopFuture` that resolves with the command's result stored in a `RESPValue`.
183185
/// If a `RedisError` is returned, the future will be failed instead.
184186
public func send(command: String, with arguments: [RESPValue]) -> EventLoopFuture<RESPValue> {
187+
self.logger.trace("Received command")
188+
185189
guard self.isConnected else {
186190
let error = RedisClientError.connectionClosed
187191
logger.warning("\(error.localizedDescription)")
@@ -203,12 +207,17 @@ extension RedisConnection {
203207
RedisMetrics.commandRoundTripTime.recordNanoseconds(duration)
204208

205209
// log the error here instead
206-
guard case let .failure(error) = result else { return }
210+
guard case let .failure(error) = result else {
211+
self.logger.trace("Command completed.")
212+
return
213+
}
207214
self.logger.error("\(error.localizedDescription)")
208215
}
209216

210217
self.logger.debug("Sending command \"\(command)\"\(arguments.count > 0 ? " with \(arguments)" : "")")
211218

219+
defer { self.logger.trace("Command sent through channel.") }
220+
212221
if self.sendCommandsImmediately {
213222
return channel.writeAndFlush(command).flatMap { promise.futureResult }
214223
} else {
@@ -228,6 +237,8 @@ extension RedisConnection {
228237
/// - Returns: A `NIO.EventLoopFuture` that resolves when the connection has been closed.
229238
@discardableResult
230239
public func close() -> EventLoopFuture<Void> {
240+
self.logger.trace("Received request to close the connection.")
241+
231242
guard self.isConnected else {
232243
// return the channel's close future, which is resolved as the last step in channel shutdown
233244
return self.channel.closeFuture
@@ -257,9 +268,9 @@ extension RedisConnection {
257268
message: .array([RESPValue(bulk: "QUIT")]),
258269
responsePromise: promise
259270
)
260-
261-
logger.debug("Sending QUIT command.")
262-
271+
272+
logger.trace("Sending QUIT command.")
273+
263274
return channel.writeAndFlush(command) // write the command
264275
.flatMap { promise.futureResult } // chain the callback to the response's
265276
.map { _ in () } // ignore the result's value
@@ -290,3 +301,15 @@ extension RedisConnection {
290301
}
291302
}
292303
}
304+
305+
// MARK: Logging
306+
307+
extension RedisConnection {
308+
/// Updates the client's logger, attaching this connection's ID to the metadata.
309+
///
310+
/// See `RedisClient.setLogging(to:)` and `RedisConnection.id`.
311+
/// - Parameter logger: The `Logging.Logger` that is desired to receive all client logs.
312+
public func setLogging(to logger: Logger) {
313+
self.logger = logger
314+
}
315+
}

Tests/RediStackIntegrationTests/RedisConnectionTests.swift

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,13 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import Logging
1516
@testable import RediStack
1617
import RediStackTestUtils
1718
import XCTest
1819

1920
final class RedisConnectionTests: RediStackIntegrationTestCase {
20-
static let expectedLogsMessage = "The following log(s) in this test are expected."
21-
2221
func test_unexpectedChannelClose() throws {
23-
print(RedisConnectionTests.expectedLogsMessage)
24-
2522
XCTAssertTrue(self.connection.isConnected)
2623
try self.connection.channel.close().wait()
2724
XCTAssertFalse(self.connection.isConnected)
@@ -35,8 +32,6 @@ final class RedisConnectionTests: RediStackIntegrationTestCase {
3532
}
3633

3734
func test_sendingCommandAfterClosing() throws {
38-
print(RedisConnectionTests.expectedLogsMessage)
39-
4035
self.connection.close()
4136
do {
4237
_ = try self.connection.ping().wait()
@@ -46,3 +41,47 @@ final class RedisConnectionTests: RediStackIntegrationTestCase {
4641
}
4742
}
4843
}
44+
45+
// MARK: Logging
46+
47+
extension RedisConnectionTests {
48+
final class TestLogHandler: LogHandler {
49+
var messages: [Logger.Message]
50+
var metadata: Logger.Metadata
51+
var logLevel: Logger.Level
52+
53+
init() {
54+
self.messages = []
55+
self.metadata = [:]
56+
self.logLevel = .trace
57+
}
58+
59+
func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, file: String, function: String, line: UInt) {
60+
self.messages.append(message)
61+
}
62+
63+
subscript(metadataKey key: String) -> Logger.Metadata.Value? {
64+
get { return self.metadata[key] }
65+
set(newValue) { self.metadata[key] = newValue }
66+
}
67+
}
68+
69+
func test_customLogging() throws {
70+
let handler = TestLogHandler()
71+
let logger = Logger(label: "test", factory: { _ in return handler })
72+
_ = try self.connection.logging(to: logger).ping().wait()
73+
XCTAssert(!handler.messages.isEmpty)
74+
}
75+
76+
func test_loggingMetadata() throws {
77+
let handler = TestLogHandler()
78+
let logger = Logger(label: #function, factory: { _ in return handler })
79+
self.connection.setLogging(to: logger)
80+
let metadataKey = String(describing: RedisConnection.self)
81+
XCTAssertTrue(handler.metadata.keys.contains(metadataKey))
82+
XCTAssertEqual(
83+
handler.metadata[metadataKey],
84+
.string(self.connection.id.description)
85+
)
86+
}
87+
}

0 commit comments

Comments
 (0)