Skip to content

Commit 9747cf2

Browse files
committed
Merge branch 'custom-logging' into 'master'
Revist user Logging configuration for connections and clients See merge request Mordil/swift-redi-stack!92
2 parents e0ebf92 + 209ba87 commit 9747cf2

File tree

5 files changed

+125
-60
lines changed

5 files changed

+125
-60
lines changed

Sources/RediStack/ChannelHandlers/RedisCommandHandler.swift

+6-19
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

+15-18
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

+19
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

+40-17
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

+45-6
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)