Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit dfb9158

Browse files
committedMay 27, 2020
logging support
Motivation: AsyncHTTPClient is not a simple piece of software and nowadays also quite stateful. To debug issues, the user may want logging. Modification: Support passing a logger to the request methods. Result: Debugging simplified.
1 parent 070c1e5 commit dfb9158

12 files changed

+666
-87
lines changed
 

‎Package.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,18 @@ let package = Package(
2525
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.7.0"),
2626
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.3.0"),
2727
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.5.1"),
28+
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
2829
],
2930
targets: [
3031
.target(
3132
name: "AsyncHTTPClient",
32-
dependencies: ["NIO", "NIOHTTP1", "NIOSSL", "NIOConcurrencyHelpers", "NIOHTTPCompression", "NIOFoundationCompat", "NIOTransportServices"]
33+
dependencies: ["NIO", "NIOHTTP1", "NIOSSL", "NIOConcurrencyHelpers", "NIOHTTPCompression",
34+
"NIOFoundationCompat", "NIOTransportServices", "Logging"]
3335
),
3436
.testTarget(
3537
name: "AsyncHTTPClientTests",
36-
dependencies: ["NIO", "NIOConcurrencyHelpers", "NIOSSL", "AsyncHTTPClient", "NIOFoundationCompat", "NIOTestUtils"]
38+
dependencies: ["NIO", "NIOConcurrencyHelpers", "NIOSSL", "AsyncHTTPClient", "NIOFoundationCompat",
39+
"NIOTestUtils", "Logging"]
3740
),
3841
]
3942
)

‎Sources/AsyncHTTPClient/ConnectionPool.swift

Lines changed: 78 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import Foundation
16+
import Logging
1617
import NIO
1718
import NIOConcurrencyHelpers
1819
import NIOHTTP1
@@ -64,21 +65,29 @@ final class ConnectionPool {
6465
/// When the pool is asked for a new connection, it creates a `Key` from the url associated to the `request`. This key
6566
/// is used to determine if there already exists an associated `HTTP1ConnectionProvider` in `providers`.
6667
/// If there is, the connection provider then takes care of leasing a new connection. If a connection provider doesn't exist, it is created.
67-
func getConnection(for request: HTTPClient.Request, preference: HTTPClient.EventLoopPreference, on eventLoop: EventLoop, deadline: NIODeadline?, setupComplete: EventLoopFuture<Void>) -> EventLoopFuture<Connection> {
68+
func getConnection(_ request: HTTPClient.Request,
69+
preference: HTTPClient.EventLoopPreference,
70+
taskEventLoop: EventLoop,
71+
deadline: NIODeadline?,
72+
setupComplete: EventLoopFuture<Void>,
73+
logger: Logger) -> EventLoopFuture<Connection> {
6874
let key = Key(request)
6975

7076
let provider: HTTP1ConnectionProvider = self.lock.withLock {
7177
if let existing = self.providers[key], existing.enqueue() {
7278
return existing
7379
} else {
7480
// Connection provider will be created with `pending = 1`
75-
let provider = HTTP1ConnectionProvider(key: key, eventLoop: eventLoop, configuration: self.configuration, pool: self)
81+
let provider = HTTP1ConnectionProvider(key: key,
82+
eventLoop: taskEventLoop,
83+
configuration: self.configuration,
84+
pool: self)
7685
self.providers[key] = provider
7786
return provider
7887
}
7988
}
8089

81-
return provider.getConnection(preference: preference, setupComplete: setupComplete)
90+
return provider.getConnection(preference: preference, setupComplete: setupComplete, logger: logger)
8291
}
8392

8493
func delete(_ provider: HTTP1ConnectionProvider) {
@@ -167,21 +176,21 @@ class Connection {
167176
/// Release this `Connection` to its associated `HTTP1ConnectionProvider`.
168177
///
169178
/// - Warning: This only releases the connection and doesn't take care of cleaning handlers in the `Channel` pipeline.
170-
func release(closing: Bool) {
179+
func release(closing: Bool, logger: Logger) {
171180
assert(self.channel.eventLoop.inEventLoop)
172-
self.provider.release(connection: self, closing: closing)
181+
self.provider.release(connection: self, closing: closing, logger: logger)
173182
}
174183

175184
/// Called when channel exceeds idle time in pool.
176-
func timeout() {
185+
func timeout(logger: Logger) {
177186
assert(self.channel.eventLoop.inEventLoop)
178-
self.provider.timeout(connection: self)
187+
self.provider.timeout(connection: self, logger: logger)
179188
}
180189

181190
/// Called when channel goes inactive while in the pool.
182-
func remoteClosed() {
191+
func remoteClosed(logger: Logger) {
183192
assert(self.channel.eventLoop.inEventLoop)
184-
self.provider.remoteClosed(connection: self)
193+
self.provider.remoteClosed(connection: self, logger: logger)
185194
}
186195

187196
func cancel() -> EventLoopFuture<Void> {
@@ -194,9 +203,10 @@ class Connection {
194203
}
195204

196205
/// Sets idle timeout handler and channel inactivity listener.
197-
func setIdleTimeout(timeout: TimeAmount?) {
206+
func setIdleTimeout(timeout: TimeAmount?, logger: Logger) {
198207
_ = self.channel.pipeline.addHandler(IdleStateHandler(writeTimeout: timeout), position: .first).flatMap { _ in
199-
self.channel.pipeline.addHandler(IdlePoolConnectionHandler(connection: self))
208+
self.channel.pipeline.addHandler(IdlePoolConnectionHandler(connection: self,
209+
logger: logger))
200210
}
201211
}
202212

@@ -280,50 +290,76 @@ class HTTP1ConnectionProvider {
280290
self.state.assertInvariants()
281291
}
282292

283-
private func execute(_ action: Action) {
293+
private func execute(_ action: Action, logger: Logger) {
284294
switch action {
285295
case .lease(let connection, let waiter):
286296
// if connection is became inactive, we create a new one.
287297
connection.cancelIdleTimeout().whenComplete { _ in
288298
if connection.isActiveEstimation {
299+
logger.trace("leasing existing connection",
300+
metadata: ["ahc-connection": connection])
289301
waiter.promise.succeed(connection)
290302
} else {
303+
logger.trace("opening fresh connection (found matching but inactive connection)",
304+
metadata: ["ahc-dead-connection": "\(connection)"])
291305
self.makeChannel(preference: waiter.preference).whenComplete { result in
292-
self.connect(result, waiter: waiter, replacing: connection)
306+
self.connect(result, waiter: waiter, replacing: connection, logger: logger)
293307
}
294308
}
295309
}
296310
case .create(let waiter):
311+
logger.trace("opening fresh connection (no connections to reuse available)")
297312
self.makeChannel(preference: waiter.preference).whenComplete { result in
298-
self.connect(result, waiter: waiter)
313+
self.connect(result, waiter: waiter, logger: logger)
299314
}
300315
case .replace(let connection, let waiter):
301316
connection.cancelIdleTimeout().flatMap {
302317
connection.close()
303318
}.whenComplete { _ in
319+
logger.trace("opening fresh connection (replacing exising connection)",
320+
metadata: ["ahc-old-connection": "\(connection)",
321+
"ahc-waiter": "\(waiter)"])
304322
self.makeChannel(preference: waiter.preference).whenComplete { result in
305-
self.connect(result, waiter: waiter, replacing: connection)
323+
self.connect(result, waiter: waiter, replacing: connection, logger: logger)
306324
}
307325
}
308326
case .park(let connection):
309-
connection.setIdleTimeout(timeout: self.configuration.maximumAllowedIdleTimeInConnectionPool)
327+
logger.trace("parking connection",
328+
metadata: ["ahc-connection": "\(connection)"])
329+
connection.setIdleTimeout(timeout: self.configuration.maximumAllowedIdleTimeInConnectionPool,
330+
logger: logger.detachingRequestInformation())
310331
case .closeProvider:
332+
logger.debug("closing provider",
333+
metadata: ["ahc-provider": "\(self)"])
311334
self.closeAndDelete()
312335
case .none:
313336
break
314337
case .parkAnd(let connection, let action):
315-
connection.setIdleTimeout(timeout: self.configuration.maximumAllowedIdleTimeInConnectionPool)
316-
self.execute(action)
338+
logger.trace("parking connection & doing further action",
339+
metadata: ["ahc-connection": "\(connection)",
340+
"ahc-action": "\(action)"])
341+
connection.setIdleTimeout(timeout: self.configuration.maximumAllowedIdleTimeInConnectionPool,
342+
logger: logger.detachingRequestInformation())
343+
self.execute(action, logger: logger)
317344
case .closeAnd(let connection, let action):
345+
logger.trace("closing connection & doing further action",
346+
metadata: ["ahc-connection": "\(connection)",
347+
"ahc-action": "\(action)"])
318348
connection.channel.close(promise: nil)
319-
self.execute(action)
349+
self.execute(action, logger: logger)
320350
case .cancel(let connection, let close):
351+
logger.trace("cancelling connection",
352+
metadata: ["ahc-connection": "\(connection)",
353+
"ahc-close": "\(close)"])
321354
connection.cancel().whenComplete { _ in
322355
if close {
323356
self.closeAndDelete()
324357
}
325358
}
326359
case .fail(let waiter, let error):
360+
logger.debug("failing connection for waiter",
361+
metadata: ["ahc-waiter": "\(waiter)",
362+
"ahc-error": "\(error)"])
327363
waiter.promise.fail(error)
328364
}
329365
}
@@ -335,19 +371,24 @@ class HTTP1ConnectionProvider {
335371
}
336372
}
337373

338-
func getConnection(preference: HTTPClient.EventLoopPreference, setupComplete: EventLoopFuture<Void>) -> EventLoopFuture<Connection> {
374+
func getConnection(preference: HTTPClient.EventLoopPreference,
375+
setupComplete: EventLoopFuture<Void>,
376+
logger: Logger) -> EventLoopFuture<Connection> {
339377
let waiter = Waiter(promise: self.eventLoop.makePromise(), setupComplete: setupComplete, preference: preference)
340378

341379
let action: Action = self.lock.withLock {
342380
self.state.acquire(waiter: waiter)
343381
}
344382

345-
self.execute(action)
383+
self.execute(action, logger: logger)
346384

347385
return waiter.promise.futureResult
348386
}
349387

350-
func connect(_ result: Result<Channel, Error>, waiter: Waiter, replacing closedConnection: Connection? = nil) {
388+
func connect(_ result: Result<Channel, Error>,
389+
waiter: Waiter,
390+
replacing closedConnection: Connection? = nil,
391+
logger: Logger) {
351392
let action: Action
352393
switch result {
353394
case .success(let channel):
@@ -366,11 +407,13 @@ class HTTP1ConnectionProvider {
366407
waiter.promise.fail(error)
367408
}
368409
waiter.setupComplete.whenComplete { _ in
369-
self.execute(action)
410+
self.execute(action, logger: logger)
370411
}
371412
}
372413

373-
func release(connection: Connection, closing: Bool) {
414+
func release(connection: Connection, closing: Bool, logger: Logger) {
415+
logger.debug("releasing connection, request complete",
416+
metadata: ["ahc-closing": "\(closing)"])
374417
let action: Action = self.lock.withLock {
375418
self.state.release(connection: connection, closing: closing)
376419
}
@@ -381,31 +424,31 @@ class HTTP1ConnectionProvider {
381424
case .park, .closeProvider:
382425
// Since both `.park` and `.deleteProvider` are terminal in terms of execution,
383426
// we can execute them immediately
384-
self.execute(action)
427+
self.execute(action, logger: logger)
385428
case .cancel, .closeAnd, .create, .fail, .lease, .parkAnd, .replace:
386429
// This is needed to start a new stack, otherwise, since this is called on a previous
387430
// future completion handler chain, it will be growing indefinitely until the connection is closed.
388431
// We might revisit this when https://github.com/apple/swift-nio/issues/970 is resolved.
389432
connection.channel.eventLoop.execute {
390-
self.execute(action)
433+
self.execute(action, logger: logger)
391434
}
392435
}
393436
}
394437

395-
func remoteClosed(connection: Connection) {
438+
func remoteClosed(connection: Connection, logger: Logger) {
396439
let action: Action = self.lock.withLock {
397440
self.state.remoteClosed(connection: connection)
398441
}
399442

400-
self.execute(action)
443+
self.execute(action, logger: logger)
401444
}
402445

403-
func timeout(connection: Connection) {
446+
func timeout(connection: Connection, logger: Logger) {
404447
let action: Action = self.lock.withLock {
405448
self.state.timeout(connection: connection)
406449
}
407450

408-
self.execute(action)
451+
self.execute(action, logger: logger)
409452
}
410453

411454
private func closeAndDelete() {
@@ -510,25 +553,27 @@ class IdlePoolConnectionHandler: ChannelInboundHandler, RemovableChannelHandler
510553

511554
let connection: Connection
512555
var eventSent: Bool
556+
let logger: Logger
513557

514-
init(connection: Connection) {
558+
init(connection: Connection, logger: Logger) {
515559
self.connection = connection
516560
self.eventSent = false
561+
self.logger = logger
517562
}
518563

519564
// this is needed to detect when remote end closes connection while connection is in the pool idling
520565
func channelInactive(context: ChannelHandlerContext) {
521566
if !self.eventSent {
522567
self.eventSent = true
523-
self.connection.remoteClosed()
568+
self.connection.remoteClosed(logger: self.logger)
524569
}
525570
}
526571

527572
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
528573
if let idleEvent = event as? IdleStateHandler.IdleStateEvent, idleEvent == .write {
529574
if !self.eventSent {
530575
self.eventSent = true
531-
self.connection.timeout()
576+
self.connection.timeout(logger: self.logger)
532577
}
533578
} else {
534579
context.fireUserInboundEventTriggered(event)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the AsyncHTTPClient project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Logging
16+
17+
internal struct DoNotLogger: LogHandler {
18+
func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, file: String, function: String, line: UInt) {}
19+
20+
subscript(metadataKey _: String) -> Logger.Metadata.Value? {
21+
get {
22+
return nil
23+
}
24+
set {}
25+
}
26+
27+
var metadata: Logger.Metadata {
28+
get {
29+
return [:]
30+
}
31+
set {}
32+
}
33+
34+
var logLevel: Logger.Level {
35+
get {
36+
return .critical
37+
}
38+
set {}
39+
}
40+
}

‎Sources/AsyncHTTPClient/HTTPClient.swift

Lines changed: 172 additions & 14 deletions
Large diffs are not rendered by default.

‎Sources/AsyncHTTPClient/HTTPHandler.swift

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import Foundation
16+
import Logging
1617
import NIO
1718
import NIOConcurrencyHelpers
1819
import NIOFoundationCompat
@@ -494,17 +495,19 @@ extension HTTPClient {
494495
var connection: Connection?
495496
var cancelled: Bool
496497
let lock: Lock
498+
let logger: Logger // We are okay to store the logger here because a Task is for only ond request.
497499

498-
init(eventLoop: EventLoop) {
500+
init(eventLoop: EventLoop, logger: Logger) {
499501
self.eventLoop = eventLoop
500502
self.promise = eventLoop.makePromise()
501503
self.completion = self.promise.futureResult.map { _ in }
502504
self.cancelled = false
503505
self.lock = Lock()
506+
self.logger = logger
504507
}
505508

506-
static func failedTask(eventLoop: EventLoop, error: Error) -> Task<Response> {
507-
let task = self.init(eventLoop: eventLoop)
509+
static func failedTask(eventLoop: EventLoop, error: Error, logger: Logger) -> Task<Response> {
510+
let task = self.init(eventLoop: eventLoop, logger: logger)
508511
task.promise.fail(error)
509512
return task
510513
}
@@ -546,13 +549,18 @@ extension HTTPClient {
546549
}
547550
}
548551

549-
func succeed<Delegate: HTTPClientResponseDelegate>(promise: EventLoopPromise<Response>?, with value: Response, delegateType: Delegate.Type, closing: Bool) {
550-
self.releaseAssociatedConnection(delegateType: delegateType, closing: closing).whenSuccess {
552+
func succeed<Delegate: HTTPClientResponseDelegate>(promise: EventLoopPromise<Response>?,
553+
with value: Response,
554+
delegateType: Delegate.Type,
555+
closing: Bool) {
556+
self.releaseAssociatedConnection(delegateType: delegateType,
557+
closing: closing).whenSuccess {
551558
promise?.succeed(value)
552559
}
553560
}
554561

555-
func fail<Delegate: HTTPClientResponseDelegate>(with error: Error, delegateType: Delegate.Type) {
562+
func fail<Delegate: HTTPClientResponseDelegate>(with error: Error,
563+
delegateType: Delegate.Type) {
556564
if let connection = self.connection {
557565
self.releaseAssociatedConnection(delegateType: delegateType, closing: true)
558566
.whenSuccess {
@@ -562,13 +570,14 @@ extension HTTPClient {
562570
}
563571
}
564572

565-
func releaseAssociatedConnection<Delegate: HTTPClientResponseDelegate>(delegateType: Delegate.Type, closing: Bool) -> EventLoopFuture<Void> {
573+
func releaseAssociatedConnection<Delegate: HTTPClientResponseDelegate>(delegateType: Delegate.Type,
574+
closing: Bool) -> EventLoopFuture<Void> {
566575
if let connection = self.connection {
567576
// remove read timeout handler
568577
return connection.removeHandler(IdleStateHandler.self).flatMap {
569578
connection.removeHandler(TaskHandler<Delegate>.self)
570579
}.map {
571-
connection.release(closing: closing)
580+
connection.release(closing: closing, logger: self.logger)
572581
}.flatMapError { error in
573582
fatalError("Couldn't remove taskHandler: \(error)")
574583
}
@@ -600,6 +609,7 @@ internal class TaskHandler<Delegate: HTTPClientResponseDelegate>: RemovableChann
600609
let delegate: Delegate
601610
let redirectHandler: RedirectHandler<Delegate.Response>?
602611
let ignoreUncleanSSLShutdown: Bool
612+
let logger: Logger // We are okay to store the logger here because a TaskHandler is just for one request.
603613

604614
var state: State = .idle
605615
var pendingRead = false
@@ -617,12 +627,14 @@ internal class TaskHandler<Delegate: HTTPClientResponseDelegate>: RemovableChann
617627
kind: HTTPClient.Request.Kind,
618628
delegate: Delegate,
619629
redirectHandler: RedirectHandler<Delegate.Response>?,
620-
ignoreUncleanSSLShutdown: Bool) {
630+
ignoreUncleanSSLShutdown: Bool,
631+
logger: Logger) {
621632
self.task = task
622633
self.delegate = delegate
623634
self.redirectHandler = redirectHandler
624635
self.ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown
625636
self.kind = kind
637+
self.logger = logger
626638
}
627639
}
628640

@@ -678,7 +690,10 @@ extension TaskHandler {
678690
do {
679691
let result = try body(self.task)
680692

681-
self.task.succeed(promise: promise, with: result, delegateType: Delegate.self, closing: self.closing)
693+
self.task.succeed(promise: promise,
694+
with: result,
695+
delegateType: Delegate.self,
696+
closing: self.closing)
682697
} catch {
683698
self.task.fail(with: error, delegateType: Delegate.self)
684699
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the AsyncHTTPClient project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
extension Connection: CustomStringConvertible {
16+
var description: String {
17+
return "\(self.channel)"
18+
}
19+
}
20+
21+
extension HTTP1ConnectionProvider.Waiter: CustomStringConvertible {
22+
var description: String {
23+
return "HTTP1ConnectionProvider.Waiter(\(self.preference))"
24+
}
25+
}
26+
27+
extension HTTPClient.EventLoopPreference: CustomStringConvertible {
28+
public var description: String {
29+
return "\(self.preference)"
30+
}
31+
}

‎Tests/AsyncHTTPClientTests/ConnectionPoolTests.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,7 +1229,7 @@ class ConnectionPoolTests: XCTestCase {
12291229
XCTAssertEqual(1, snapshot.pending)
12301230
XCTAssertEqual(1, snapshot.openedConnectionsCount)
12311231

1232-
connection.release(closing: false)
1232+
connection.release(closing: false, logger: HTTPClient.loggingDisabled)
12331233

12341234
// XCTAssertFalse(connection.isInUse)
12351235
snapshot = provider.state.testsOnly_getInternalState()
@@ -1263,7 +1263,7 @@ class ConnectionPoolTests: XCTestCase {
12631263
XCTAssertEqual(1, snapshot.pending)
12641264
XCTAssertEqual(1, snapshot.openedConnectionsCount)
12651265

1266-
connection.release(closing: true)
1266+
connection.release(closing: true, logger: HTTPClient.loggingDisabled)
12671267

12681268
snapshot = provider.state.testsOnly_getInternalState()
12691269
XCTAssertEqual(0, snapshot.availableConnections.count)
@@ -1296,7 +1296,7 @@ class ConnectionPoolTests: XCTestCase {
12961296
XCTAssertEqual(1, snapshot.pending)
12971297
XCTAssertEqual(1, snapshot.openedConnectionsCount)
12981298

1299-
connection.remoteClosed()
1299+
connection.remoteClosed(logger: HTTPClient.loggingDisabled)
13001300

13011301
snapshot = provider.state.testsOnly_getInternalState()
13021302
XCTAssertEqual(0, snapshot.availableConnections.count)
@@ -1329,7 +1329,7 @@ class ConnectionPoolTests: XCTestCase {
13291329
XCTAssertEqual(1, snapshot.pending)
13301330
XCTAssertEqual(1, snapshot.openedConnectionsCount)
13311331

1332-
connection.timeout()
1332+
connection.timeout(logger: HTTPClient.loggingDisabled)
13331333

13341334
snapshot = provider.state.testsOnly_getInternalState()
13351335
XCTAssertEqual(0, snapshot.availableConnections.count)
@@ -1367,7 +1367,7 @@ class ConnectionPoolTests: XCTestCase {
13671367
switch action {
13681368
case .lease(let connection, let waiter):
13691369
// Since this connection is already in use, this should be a no-op and state should not have changed from normal lease
1370-
connection.timeout()
1370+
connection.timeout(logger: HTTPClient.loggingDisabled)
13711371

13721372
snapshot = provider.state.testsOnly_getInternalState()
13731373
XCTAssertTrue(connection.isActiveEstimation)
@@ -1380,7 +1380,7 @@ class ConnectionPoolTests: XCTestCase {
13801380

13811381
// This is unrecoverable, but in this case we create a new connection, so state again should not change, even though release will be called
13821382
// This is important to prevent provider deletion since connection is released and there could be 0 waiters
1383-
connection.remoteClosed()
1383+
connection.remoteClosed(logger: HTTPClient.loggingDisabled)
13841384

13851385
snapshot = provider.state.testsOnly_getInternalState()
13861386
XCTAssertTrue(snapshot.leasedConnections.contains(ConnectionKey(connection)))

‎Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,15 @@ class HTTPClientInternalTests: XCTestCase {
3939
func testHTTPPartsHandler() throws {
4040
let channel = EmbeddedChannel()
4141
let recorder = RecordingHandler<HTTPClientResponsePart, HTTPClientRequestPart>()
42-
let task = Task<Void>(eventLoop: channel.eventLoop)
42+
let task = Task<Void>(eventLoop: channel.eventLoop, logger: HTTPClient.loggingDisabled)
4343

4444
try channel.pipeline.addHandler(recorder).wait()
4545
try channel.pipeline.addHandler(TaskHandler(task: task,
4646
kind: .host,
4747
delegate: TestHTTPDelegate(),
4848
redirectHandler: nil,
49-
ignoreUncleanSSLShutdown: false)).wait()
49+
ignoreUncleanSSLShutdown: false,
50+
logger: HTTPClient.loggingDisabled)).wait()
5051

5152
var request = try Request(url: "http://localhost/get")
5253
request.headers.add(name: "X-Test-Header", value: "X-Test-Value")
@@ -69,14 +70,15 @@ class HTTPClientInternalTests: XCTestCase {
6970
func testBadHTTPRequest() throws {
7071
let channel = EmbeddedChannel()
7172
let recorder = RecordingHandler<HTTPClientResponsePart, HTTPClientRequestPart>()
72-
let task = Task<Void>(eventLoop: channel.eventLoop)
73+
let task = Task<Void>(eventLoop: channel.eventLoop, logger: HTTPClient.loggingDisabled)
7374

7475
XCTAssertNoThrow(try channel.pipeline.addHandler(recorder).wait())
7576
XCTAssertNoThrow(try channel.pipeline.addHandler(TaskHandler(task: task,
7677
kind: .host,
7778
delegate: TestHTTPDelegate(),
7879
redirectHandler: nil,
79-
ignoreUncleanSSLShutdown: false)).wait())
80+
ignoreUncleanSSLShutdown: false,
81+
logger: HTTPClient.loggingDisabled)).wait())
8082

8183
var request = try Request(url: "http://localhost/get")
8284
request.headers.add(name: "X-Test-Header", value: "X-Test-Value")
@@ -91,12 +93,13 @@ class HTTPClientInternalTests: XCTestCase {
9193
func testHTTPPartsHandlerMultiBody() throws {
9294
let channel = EmbeddedChannel()
9395
let delegate = TestHTTPDelegate()
94-
let task = Task<Void>(eventLoop: channel.eventLoop)
96+
let task = Task<Void>(eventLoop: channel.eventLoop, logger: HTTPClient.loggingDisabled)
9597
let handler = TaskHandler(task: task,
9698
kind: .host,
9799
delegate: delegate,
98100
redirectHandler: nil,
99-
ignoreUncleanSSLShutdown: false)
101+
ignoreUncleanSSLShutdown: false,
102+
logger: HTTPClient.loggingDisabled)
100103

101104
try channel.pipeline.addHandler(handler).wait()
102105

@@ -573,19 +576,20 @@ class HTTPClientInternalTests: XCTestCase {
573576
// This is pretty evil but we literally just get hold of a connection to get to the channel to be able to
574577
// observe when the server closing the connection is known to the client.
575578
let el = group.next()
576-
XCTAssertNoThrow(maybeConnection = try client.pool.getConnection(for: .init(url: url),
579+
XCTAssertNoThrow(maybeConnection = try client.pool.getConnection(Request(url: url),
577580
preference: .indifferent,
578-
on: el,
581+
taskEventLoop: el,
579582
deadline: nil,
580-
setupComplete: el.makeSucceededFuture(())).wait())
583+
setupComplete: el.makeSucceededFuture(()),
584+
logger: HTTPClient.loggingDisabled).wait())
581585
guard let connection = maybeConnection else {
582586
XCTFail("couldn't get connection")
583587
return
584588
}
585589

586590
// And let's also give the connection back :).
587591
try connection.channel.eventLoop.submit {
588-
connection.release(closing: false)
592+
connection.release(closing: false, logger: HTTPClient.loggingDisabled)
589593
}.wait()
590594

591595
XCTAssertEqual(0, sharedStateServerHandler.requestNumber.load())
@@ -622,19 +626,20 @@ class HTTPClientInternalTests: XCTestCase {
622626
body: nil)
623627
var maybeConnection: Connection?
624628
let el = client.eventLoopGroup.next()
625-
XCTAssertNoThrow(try maybeConnection = client.pool.getConnection(for: req,
629+
XCTAssertNoThrow(try maybeConnection = client.pool.getConnection(req,
626630
preference: .indifferent,
627-
on: el,
631+
taskEventLoop: el,
628632
deadline: nil,
629-
setupComplete: el.makeSucceededFuture(())).wait())
633+
setupComplete: el.makeSucceededFuture(()),
634+
logger: HTTPClient.loggingDisabled).wait())
630635
guard let connection = maybeConnection else {
631636
XCTFail("couldn't make connection")
632637
throw NoChannelError()
633638
}
634639

635640
let channel = connection.channel
636641
try! channel.eventLoop.submit {
637-
connection.release(closing: true)
642+
connection.release(closing: true, logger: HTTPClient.loggingDisabled)
638643
}.wait()
639644
return (web, channel)
640645
})
@@ -707,11 +712,12 @@ class HTTPClientInternalTests: XCTestCase {
707712
// Let's start by getting a connection so we can mess with the Channel :).
708713
var maybeConnection: Connection?
709714
let el = client.eventLoopGroup.next()
710-
XCTAssertNoThrow(try maybeConnection = client.pool.getConnection(for: req,
715+
XCTAssertNoThrow(try maybeConnection = client.pool.getConnection(req,
711716
preference: .indifferent,
712-
on: el,
717+
taskEventLoop: el,
713718
deadline: nil,
714-
setupComplete: el.makeSucceededFuture(())).wait())
719+
setupComplete: el.makeSucceededFuture(()),
720+
logger: HTTPClient.loggingDisabled).wait())
715721
guard let connection = maybeConnection else {
716722
XCTFail("couldn't make connection")
717723
return
@@ -725,7 +731,7 @@ class HTTPClientInternalTests: XCTestCase {
725731
sawTheClosePromise: sawTheClosePromise),
726732
position: .first).wait())
727733
try! connection.channel.eventLoop.submit {
728-
connection.release(closing: false)
734+
connection.release(closing: false, logger: HTTPClient.loggingDisabled)
729735
}.wait()
730736

731737
XCTAssertNoThrow(try client.execute(request: req).wait())
@@ -742,11 +748,12 @@ class HTTPClientInternalTests: XCTestCase {
742748
// When asking for a connection again, we should _not_ get the same one back because we did most of the close,
743749
// similar to what the SSLHandler would do.
744750
let el2 = client.eventLoopGroup.next()
745-
let connection2Future = client.pool.getConnection(for: req,
751+
let connection2Future = client.pool.getConnection(req,
746752
preference: .indifferent,
747-
on: el2,
753+
taskEventLoop: el2,
748754
deadline: nil,
749-
setupComplete: el2.makeSucceededFuture(()))
755+
setupComplete: el2.makeSucceededFuture(()),
756+
logger: HTTPClient.loggingDisabled)
750757
doActualCloseNowPromise.succeed(())
751758

752759
XCTAssertNoThrow(try maybeConnection = connection2Future.wait())
@@ -757,7 +764,7 @@ class HTTPClientInternalTests: XCTestCase {
757764

758765
XCTAssert(connection !== connection2)
759766
try! connection2.channel.eventLoop.submit {
760-
connection2.release(closing: true)
767+
connection2.release(closing: true, logger: HTTPClient.loggingDisabled)
761768
}.wait()
762769
XCTAssertTrue(connection2.channel.isActive)
763770
}

‎Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import AsyncHTTPClient
1616
import Foundation
17+
import Logging
1718
import NIO
1819
import NIOConcurrencyHelpers
1920
import NIOHTTP1
@@ -753,6 +754,60 @@ extension EventLoopFuture {
753754
}
754755
}
755756

757+
struct CollectEverythingLogHandler: LogHandler {
758+
var metadata: Logger.Metadata = [:]
759+
var logLevel: Logger.Level = .info
760+
let logStore: LogStore
761+
762+
class LogStore {
763+
struct Entry {
764+
var level: Logger.Level
765+
var message: String
766+
var metadata: [String: String]
767+
}
768+
769+
var lock = Lock()
770+
var logs: [Entry] = []
771+
772+
var allEntries: [Entry] {
773+
get {
774+
return self.lock.withLock { self.logs }
775+
}
776+
set {
777+
self.lock.withLock { self.logs = newValue }
778+
}
779+
}
780+
781+
func append(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?) {
782+
self.lock.withLock {
783+
self.logs.append(Entry(level: level,
784+
message: message.description,
785+
metadata: metadata?.mapValues { $0.description } ?? [:]))
786+
}
787+
}
788+
}
789+
790+
init(logStore: LogStore) {
791+
self.logStore = logStore
792+
}
793+
794+
func log(level: Logger.Level,
795+
message: Logger.Message,
796+
metadata: Logger.Metadata?,
797+
file: String, function: String, line: UInt) {
798+
self.logStore.append(level: level, message: message, metadata: self.metadata.merging(metadata ?? [:]) { $1 })
799+
}
800+
801+
subscript(metadataKey key: String) -> Logger.Metadata.Value? {
802+
get {
803+
return self.metadata[key]
804+
}
805+
set {
806+
self.metadata[key] = newValue
807+
}
808+
}
809+
}
810+
756811
private let cert = """
757812
-----BEGIN CERTIFICATE-----
758813
MIICmDCCAYACCQCPC8JDqMh1zzANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJ1

‎Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ extension HTTPClientTests {
103103
("testWeHandleUsReceivingACloseHeaderCorrectly", testWeHandleUsReceivingACloseHeaderCorrectly),
104104
("testWeHandleUsSendingACloseHeaderAmongstOtherConnectionHeadersCorrectly", testWeHandleUsSendingACloseHeaderAmongstOtherConnectionHeadersCorrectly),
105105
("testWeHandleUsReceivingACloseHeaderAmongstOtherConnectionHeadersCorrectly", testWeHandleUsReceivingACloseHeaderAmongstOtherConnectionHeadersCorrectly),
106+
("testLoggingCorrectlyAttachesRequestInformation", testLoggingCorrectlyAttachesRequestInformation),
107+
("testNothingIsLoggedAtInfoOrHigher", testNothingIsLoggedAtInfoOrHigher),
108+
("testAllMethodsLog", testAllMethodsLog),
106109
]
107110
}
108111
}

‎Tests/AsyncHTTPClientTests/HTTPClientTests.swift

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#if canImport(Network)
1717
import Network
1818
#endif
19+
import Logging
1920
import NIO
2021
import NIOConcurrencyHelpers
2122
import NIOFoundationCompat
@@ -1756,4 +1757,174 @@ class HTTPClientTests: XCTestCase {
17561757
XCTAssertEqual(stats2.connectionNumber, stats3.connectionNumber)
17571758
}
17581759
}
1760+
1761+
func testLoggingCorrectlyAttachesRequestInformation() {
1762+
let logStore = CollectEverythingLogHandler.LogStore()
1763+
1764+
var loggerYolo001: Logger = Logger(label: "\(#function)", factory: { _ in
1765+
CollectEverythingLogHandler(logStore: logStore)
1766+
})
1767+
loggerYolo001.logLevel = .trace
1768+
loggerYolo001[metadataKey: "yolo-request-id"] = "yolo-001"
1769+
var loggerACME002: Logger = Logger(label: "\(#function)", factory: { _ in
1770+
CollectEverythingLogHandler(logStore: logStore)
1771+
})
1772+
loggerACME002.logLevel = .trace
1773+
loggerACME002[metadataKey: "acme-request-id"] = "acme-002"
1774+
1775+
guard let request1 = try? HTTPClient.Request(url: self.defaultHTTPBinURLPrefix + "get"),
1776+
let request2 = try? HTTPClient.Request(url: self.defaultHTTPBinURLPrefix + "stats"),
1777+
let request3 = try? HTTPClient.Request(url: self.defaultHTTPBinURLPrefix + "ok") else {
1778+
XCTFail("bad stuff, can't even make request structures")
1779+
return
1780+
}
1781+
1782+
// === Request 1 (Yolo001)
1783+
XCTAssertNoThrow(try self.defaultClient.execute(request: request1,
1784+
eventLoop: .indifferent,
1785+
deadline: nil,
1786+
logger: loggerYolo001).wait())
1787+
let logsAfterReq1 = logStore.allEntries
1788+
logStore.allEntries = []
1789+
1790+
// === Request 2 (Yolo001)
1791+
XCTAssertNoThrow(try self.defaultClient.execute(request: request2,
1792+
eventLoop: .indifferent,
1793+
deadline: nil,
1794+
logger: loggerYolo001).wait())
1795+
let logsAfterReq2 = logStore.allEntries
1796+
logStore.allEntries = []
1797+
1798+
// === Request 3 (ACME002)
1799+
XCTAssertNoThrow(try self.defaultClient.execute(request: request3,
1800+
eventLoop: .indifferent,
1801+
deadline: nil,
1802+
logger: loggerACME002).wait())
1803+
let logsAfterReq3 = logStore.allEntries
1804+
logStore.allEntries = []
1805+
1806+
// === Assertions
1807+
XCTAssertGreaterThan(logsAfterReq1.count, 0)
1808+
XCTAssertGreaterThan(logsAfterReq2.count, 0)
1809+
XCTAssertGreaterThan(logsAfterReq3.count, 0)
1810+
1811+
XCTAssert(logsAfterReq1.allSatisfy { entry in
1812+
if let httpRequestMetadata = entry.metadata["ahc-request"],
1813+
let yoloRequestID = entry.metadata["yolo-request-id"] {
1814+
XCTAssertNil(entry.metadata["acme-request-id"])
1815+
XCTAssertEqual("yolo-001", yoloRequestID)
1816+
XCTAssertEqual("GET \(self.defaultHTTPBinURLPrefix)get", httpRequestMetadata)
1817+
return true
1818+
} else {
1819+
XCTFail("log message doesn't contain the right IDs: \(entry)")
1820+
return false
1821+
}
1822+
})
1823+
XCTAssert(logsAfterReq1.contains { entry in
1824+
entry.message == "opening fresh connection (no connections to reuse available)"
1825+
})
1826+
1827+
XCTAssert(logsAfterReq2.allSatisfy { entry in
1828+
if let httpRequestMetadata = entry.metadata["ahc-request"],
1829+
let yoloRequestID = entry.metadata["yolo-request-id"] {
1830+
XCTAssertNil(entry.metadata["acme-request-id"])
1831+
XCTAssertEqual("yolo-001", yoloRequestID)
1832+
XCTAssertEqual("GET \(self.defaultHTTPBinURLPrefix)stats", httpRequestMetadata)
1833+
return true
1834+
} else {
1835+
XCTFail("log message doesn't contain the right IDs: \(entry)")
1836+
return false
1837+
}
1838+
})
1839+
XCTAssert(logsAfterReq2.contains { entry in
1840+
entry.message.starts(with: "reusing active connection ")
1841+
})
1842+
1843+
XCTAssert(logsAfterReq3.allSatisfy { entry in
1844+
if let httpRequestMetadata = entry.metadata["ahc-request"],
1845+
let acmeRequestID = entry.metadata["acme-request-id"] {
1846+
XCTAssertNil(entry.metadata["yolo-request-id"])
1847+
XCTAssertEqual("acme-002", acmeRequestID)
1848+
XCTAssertEqual("GET \(self.defaultHTTPBinURLPrefix)ok", httpRequestMetadata)
1849+
return true
1850+
} else {
1851+
XCTFail("log message doesn't contain the right IDs: \(entry)")
1852+
return false
1853+
}
1854+
})
1855+
XCTAssert(logsAfterReq3.contains { entry in
1856+
entry.message.starts(with: "reusing active connection ")
1857+
})
1858+
}
1859+
1860+
func testNothingIsLoggedAtInfoOrHigher() {
1861+
let logStore = CollectEverythingLogHandler.LogStore()
1862+
1863+
var logger: Logger = Logger(label: "\(#function)", factory: { _ in
1864+
CollectEverythingLogHandler(logStore: logStore)
1865+
})
1866+
logger.logLevel = .info
1867+
1868+
guard let request1 = try? HTTPClient.Request(url: self.defaultHTTPBinURLPrefix + "get"),
1869+
let request2 = try? HTTPClient.Request(url: self.defaultHTTPBinURLPrefix + "stats") else {
1870+
XCTFail("bad stuff, can't even make request structures")
1871+
return
1872+
}
1873+
1874+
// === Request 1
1875+
XCTAssertNoThrow(try self.defaultClient.execute(request: request1,
1876+
eventLoop: .indifferent,
1877+
deadline: nil,
1878+
logger: logger).wait())
1879+
XCTAssertEqual(0, logStore.allEntries.count)
1880+
1881+
// === Request 2
1882+
XCTAssertNoThrow(try self.defaultClient.execute(request: request2,
1883+
eventLoop: .indifferent,
1884+
deadline: nil,
1885+
logger: logger).wait())
1886+
XCTAssertEqual(0, logStore.allEntries.count)
1887+
}
1888+
1889+
func testAllMethodsLog() {
1890+
func checkExpectationsWithLogger<T>(type: String, _ body: (Logger, String) throws -> T) throws -> T {
1891+
let logStore = CollectEverythingLogHandler.LogStore()
1892+
1893+
var logger: Logger = Logger(label: "\(#function)", factory: { _ in
1894+
CollectEverythingLogHandler(logStore: logStore)
1895+
})
1896+
logger.logLevel = .trace
1897+
logger[metadataKey: "req"] = "yo-\(type)"
1898+
1899+
let url = self.defaultHTTPBinURLPrefix + "not-found/request/\(type))"
1900+
let result = try body(logger, url)
1901+
1902+
XCTAssertGreaterThan(logStore.allEntries.count, 0)
1903+
logStore.allEntries.forEach { entry in
1904+
XCTAssertEqual("yo-\(type)", entry.metadata["req"] ?? "n/a")
1905+
XCTAssertEqual("\(type) \(url)", entry.metadata["ahc-request"] ?? "n/a")
1906+
}
1907+
return result
1908+
}
1909+
1910+
XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "GET") { logger, url in
1911+
try self.defaultClient.get(url: url, logger: logger).wait()
1912+
}.status))
1913+
1914+
XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "PUT") { logger, url in
1915+
try self.defaultClient.put(url: url, logger: logger).wait()
1916+
}.status))
1917+
1918+
XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "POST") { logger, url in
1919+
try self.defaultClient.post(url: url, logger: logger).wait()
1920+
}.status))
1921+
1922+
XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "DELETE") { logger, url in
1923+
try self.defaultClient.delete(url: url, logger: logger).wait()
1924+
}.status))
1925+
1926+
XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "PATCH") { logger, url in
1927+
try self.defaultClient.patch(url: url, logger: logger).wait()
1928+
}.status))
1929+
}
17591930
}

‎docs/logging-design.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Design of the way AsyncHTTPClient logs
2+
3+
<details>
4+
<summary>Unless you explicitly pass AsyncHTTPClient a `Logger` instance, nothing is ever logged.</summary>
5+
AsyncHTTPClient is useful in many places where you wouldn't want to log, for example a command line HTTP client. Also, we do not want to change its default behaviour in a minor release.
6+
</details>
7+
<details>
8+
<summary>Nothing is logged at level `info` or higher, unless something is really wrong that cannot be communicated through the API.</summary>
9+
Fundamentally, AsyncHTTPClient performs a simple task, it makes a HTTP request and communicates the outcome back via its API. In normal usage, we would not expect people to want AsyncHTTPClient to log. In certain scenarios, for example when debugging why a request takes longer than expected it may however be useful to get information about AsyncHTTPClient's connection pool. That is when enabling logging may become useful.
10+
</details>
11+
<details>
12+
<summary>Each request will get a globally unique request ID (`ahc-request-id`) that will be attached (as metadata) to each log message relevant to a request.</summary>
13+
When many concurrent requests are active, it can be challenging to figure out which log message is associated with which request. To facilitate this task, AsyncHTTPClient will add a metadata field `ahc-request-id` to each log message so you can first find the request ID that is causing issues and then filter only messages with that ID.
14+
</details>
15+
<details>
16+
<summary>Your `Logger` metadata is preserved.</summary>
17+
AsyncHTTPClient accepts a `Logger` on every request method. This means that all the metadata you have attached, will be present on log messages issued by AsyncHTTPClient.
18+
19+
For example, if you attach `["my-system-req-uuid": "84B453E0-0DFD-4B4B-BF22-3434812C9015"]` and then do two requests using AsyncHTTPClient, both of those requests will carry `"my-system-req-uuid` as well as AsyncHTTPClient's `ahc-request-id`. This allows you to filter all HTTP request made from one of your system's requests whilst still disambiguating the HTTP requests (they will have different `ahc-request-id`s.
20+
</details>
21+
<details>
22+
<summary>Instead of accepting one `Logger` instance per `HTTPClient` instance, each request method can accept a `Logger`.</summary>
23+
This allows AsyncHTTPClient to preserve your metadata and add its own metadata such as `ahc-request-id`.
24+
</details>
25+
<details>
26+
<summary>All logs use the structured logging pattern, i.e. only static log messages and accompanying key/value metadata are used.</summary>
27+
None of the log messages issued by AsyncHTTPClient will use String interpolation which means they will always be the exact same message.
28+
29+
For example when AsyncHTTPClient wants to tell you it got an actual network connection to perform a request on, it will give the logger the following pieces of information:
30+
31+
- message: `got connection for request`
32+
- metadata (the values are example):
33+
- `ahc-request-id`: `0`
34+
- `ahc-connection`: `SocketChannel { BaseSocket { fd=15 }, active = true, localAddress = Optional([IPv4]127.0.0.1/127.0.0.1:54459), remoteAddress = Optional([IPv4]127.0.0.1/127.0.0.1:54457) }`
35+
36+
As you can see above, the log message doesn't actually contain the request or the network connection. Both of those pieces of information are in the `metadata`.
37+
38+
The rationale is that many people use log aggregation systems where it is very useful to aggregate, search and group by log message, or specific metadata values. This is greatly simplified by using a constant string (relatively stable) string and explicitly marked metadata values which make it easy to filter by.
39+
</details>
40+
<details>
41+
<summary>`debug` should be enough to diagnose most problems but information that can be correlated is usually skipped.</summary>
42+
When crafting log messages, it's often hard to strike a balance between logging everything and logging just enough. A rule of thumb is that you have to assume someone may be running with `logLevel = .debug` in production. So it can't be too much. Yet `.trace` can log everything you would need to know when debugging a tricky implementation issue. We assume nobody is running in production with `logLevel = .trace`.
43+
44+
The problem with logging everything is that logging itself becomes very slow. We want logging in `debug` level to still be reasonably performant and therefore avoid logging information that can be correlated from other log messages.
45+
46+
For example, AsyncHTTPClient may tell you in two log messages that it `got a connection` (from the connection pool) and a little later that it's `parking connection` (in the connection pool). Just like all messages, both of them will have an associated `ahc-request-id` which makes it possible to correlate the two log messages. The message that logs that we actually got a network connection will also include information about this network connection. The message that we're now parking the connection however _will not_. The information which connection is being parked can be found by filtering all other log messages with the same `ahc-request-id`.
47+
</details>
48+
<details>
49+
<summary>In `trace`, AsyncHTTPClient may log _a lot_.</summary>
50+
In the `.trace` log level, AsyncHTTPClient basically logs all the information that it has handily available. The frugality considerations we take in `.debug` do not apply here. We just want to log as much information as possible. This is useful almost exclusively for local debugging and should almost certainly not be sent into a log aggregation system where the information might be persisted for a long time. This also means, handing AsyncHTTPClient a logger in `logLevel = .trace` may have a fairly serious performance impact.
51+
</details>

0 commit comments

Comments
 (0)
Please sign in to comment.