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 ccc5d13

Browse files
committedJun 2, 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 364d106 commit ccc5d13

13 files changed

+1119
-418
lines changed
 

‎.swiftformat

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@
99
--patternlet inline
1010
--stripunusedargs unnamed-only
1111
--ranges nospace
12+
--disable typeSugar # https://github.com/nicklockwood/SwiftFormat/issues/636
1213

1314
# rules

‎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: 97 additions & 35 deletions
Large diffs are not rendered by default.

‎Sources/AsyncHTTPClient/HTTPClient.swift

Lines changed: 208 additions & 25 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
@@ -533,17 +534,19 @@ extension HTTPClient {
533534
var connection: Connection?
534535
var cancelled: Bool
535536
let lock: Lock
537+
let logger: Logger // We are okay to store the logger here because a Task is for only ond request.
536538

537-
init(eventLoop: EventLoop) {
539+
init(eventLoop: EventLoop, logger: Logger) {
538540
self.eventLoop = eventLoop
539541
self.promise = eventLoop.makePromise()
540542
self.completion = self.promise.futureResult.map { _ in }
541543
self.cancelled = false
542544
self.lock = Lock()
545+
self.logger = logger
543546
}
544547

545-
static func failedTask(eventLoop: EventLoop, error: Error) -> Task<Response> {
546-
let task = self.init(eventLoop: eventLoop)
548+
static func failedTask(eventLoop: EventLoop, error: Error, logger: Logger) -> Task<Response> {
549+
let task = self.init(eventLoop: eventLoop, logger: logger)
547550
task.promise.fail(error)
548551
return task
549552
}
@@ -585,13 +588,18 @@ extension HTTPClient {
585588
}
586589
}
587590

588-
func succeed<Delegate: HTTPClientResponseDelegate>(promise: EventLoopPromise<Response>?, with value: Response, delegateType: Delegate.Type, closing: Bool) {
589-
self.releaseAssociatedConnection(delegateType: delegateType, closing: closing).whenSuccess {
591+
func succeed<Delegate: HTTPClientResponseDelegate>(promise: EventLoopPromise<Response>?,
592+
with value: Response,
593+
delegateType: Delegate.Type,
594+
closing: Bool) {
595+
self.releaseAssociatedConnection(delegateType: delegateType,
596+
closing: closing).whenSuccess {
590597
promise?.succeed(value)
591598
}
592599
}
593600

594-
func fail<Delegate: HTTPClientResponseDelegate>(with error: Error, delegateType: Delegate.Type) {
601+
func fail<Delegate: HTTPClientResponseDelegate>(with error: Error,
602+
delegateType: Delegate.Type) {
595603
if let connection = self.connection {
596604
self.releaseAssociatedConnection(delegateType: delegateType, closing: true)
597605
.whenSuccess {
@@ -601,13 +609,14 @@ extension HTTPClient {
601609
}
602610
}
603611

604-
func releaseAssociatedConnection<Delegate: HTTPClientResponseDelegate>(delegateType: Delegate.Type, closing: Bool) -> EventLoopFuture<Void> {
612+
func releaseAssociatedConnection<Delegate: HTTPClientResponseDelegate>(delegateType: Delegate.Type,
613+
closing: Bool) -> EventLoopFuture<Void> {
605614
if let connection = self.connection {
606615
// remove read timeout handler
607616
return connection.removeHandler(IdleStateHandler.self).flatMap {
608617
connection.removeHandler(TaskHandler<Delegate>.self)
609618
}.map {
610-
connection.release(closing: closing)
619+
connection.release(closing: closing, logger: self.logger)
611620
}.flatMapError { error in
612621
fatalError("Couldn't remove taskHandler: \(error)")
613622
}
@@ -639,6 +648,7 @@ internal class TaskHandler<Delegate: HTTPClientResponseDelegate>: RemovableChann
639648
let delegate: Delegate
640649
let redirectHandler: RedirectHandler<Delegate.Response>?
641650
let ignoreUncleanSSLShutdown: Bool
651+
let logger: Logger // We are okay to store the logger here because a TaskHandler is just for one request.
642652

643653
var state: State = .idle
644654
var pendingRead = false
@@ -656,12 +666,14 @@ internal class TaskHandler<Delegate: HTTPClientResponseDelegate>: RemovableChann
656666
kind: HTTPClient.Request.Kind,
657667
delegate: Delegate,
658668
redirectHandler: RedirectHandler<Delegate.Response>?,
659-
ignoreUncleanSSLShutdown: Bool) {
669+
ignoreUncleanSSLShutdown: Bool,
670+
logger: Logger) {
660671
self.task = task
661672
self.delegate = delegate
662673
self.redirectHandler = redirectHandler
663674
self.ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown
664675
self.kind = kind
676+
self.logger = logger
665677
}
666678
}
667679

@@ -717,7 +729,10 @@ extension TaskHandler {
717729
do {
718730
let result = try body(self.task)
719731

720-
self.task.succeed(promise: promise, with: result, delegateType: Delegate.self, closing: self.closing)
732+
self.task.succeed(promise: promise,
733+
with: result,
734+
delegateType: Delegate.self,
735+
closing: self.closing)
721736
} catch {
722737
self.task.fail(with: error, delegateType: Delegate.self)
723738
}
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 NoOpLogHandler: 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+
}
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: 343 additions & 320 deletions
Large diffs are not rendered by default.

‎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
@@ -752,6 +753,60 @@ extension EventLoopFuture {
752753
}
753754
}
754755

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

‎Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ extension HTTPClientTests {
105105
("testWeHandleUsReceivingACloseHeaderCorrectly", testWeHandleUsReceivingACloseHeaderCorrectly),
106106
("testWeHandleUsSendingACloseHeaderAmongstOtherConnectionHeadersCorrectly", testWeHandleUsSendingACloseHeaderAmongstOtherConnectionHeadersCorrectly),
107107
("testWeHandleUsReceivingACloseHeaderAmongstOtherConnectionHeadersCorrectly", testWeHandleUsReceivingACloseHeaderAmongstOtherConnectionHeadersCorrectly),
108+
("testLoggingCorrectlyAttachesRequestInformation", testLoggingCorrectlyAttachesRequestInformation),
109+
("testNothingIsLoggedAtInfoOrHigher", testNothingIsLoggedAtInfoOrHigher),
110+
("testAllMethodsLog", testAllMethodsLog),
111+
("testClosingIdleConnectionsInPoolLogsInTheBackground", testClosingIdleConnectionsInPoolLogsInTheBackground),
108112
]
109113
}
110114
}

‎Tests/AsyncHTTPClientTests/HTTPClientTests.swift

Lines changed: 211 additions & 4 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
@@ -33,6 +34,7 @@ class HTTPClientTests: XCTestCase {
3334
var serverGroup: EventLoopGroup!
3435
var defaultHTTPBin: HTTPBin!
3536
var defaultClient: HTTPClient!
37+
var backgroundLogStore: CollectEverythingLogHandler.LogStore!
3638

3739
var defaultHTTPBinURLPrefix: String {
3840
return "http://localhost:\(self.defaultHTTPBin.port)/"
@@ -43,16 +45,25 @@ class HTTPClientTests: XCTestCase {
4345
XCTAssertNil(self.serverGroup)
4446
XCTAssertNil(self.defaultHTTPBin)
4547
XCTAssertNil(self.defaultClient)
48+
XCTAssertNil(self.backgroundLogStore)
49+
4650
self.clientGroup = getDefaultEventLoopGroup(numberOfThreads: 1)
4751
self.serverGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
4852
self.defaultHTTPBin = HTTPBin()
49-
self.defaultClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup))
53+
self.backgroundLogStore = CollectEverythingLogHandler.LogStore()
54+
var backgroundLogger = Logger(label: "\(#function)", factory: { _ in
55+
CollectEverythingLogHandler(logStore: self.backgroundLogStore!)
56+
})
57+
backgroundLogger.logLevel = .trace
58+
self.defaultClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
59+
backgroundActivityLogger: backgroundLogger)
5060
}
5161

5262
override func tearDown() {
53-
XCTAssertNotNil(self.defaultClient)
54-
XCTAssertNoThrow(try self.defaultClient.syncShutdown())
55-
self.defaultClient = nil
63+
if let defaultClient = self.defaultClient {
64+
XCTAssertNoThrow(try defaultClient.syncShutdown())
65+
self.defaultClient = nil
66+
}
5667

5768
XCTAssertNotNil(self.defaultHTTPBin)
5869
XCTAssertNoThrow(try self.defaultHTTPBin.shutdown())
@@ -65,6 +76,9 @@ class HTTPClientTests: XCTestCase {
6576
XCTAssertNotNil(self.serverGroup)
6677
XCTAssertNoThrow(try self.serverGroup.syncShutdownGracefully())
6778
self.serverGroup = nil
79+
80+
XCTAssertNotNil(self.backgroundLogStore)
81+
self.backgroundLogStore = nil
6882
}
6983

7084
func testRequestURI() throws {
@@ -1793,4 +1807,197 @@ class HTTPClientTests: XCTestCase {
17931807
XCTAssertEqual(stats2.connectionNumber, stats3.connectionNumber)
17941808
}
17951809
}
1810+
1811+
func testLoggingCorrectlyAttachesRequestInformation() {
1812+
let logStore = CollectEverythingLogHandler.LogStore()
1813+
1814+
var loggerYolo001: Logger = Logger(label: "\(#function)", factory: { _ in
1815+
CollectEverythingLogHandler(logStore: logStore)
1816+
})
1817+
loggerYolo001.logLevel = .trace
1818+
loggerYolo001[metadataKey: "yolo-request-id"] = "yolo-001"
1819+
var loggerACME002: Logger = Logger(label: "\(#function)", factory: { _ in
1820+
CollectEverythingLogHandler(logStore: logStore)
1821+
})
1822+
loggerACME002.logLevel = .trace
1823+
loggerACME002[metadataKey: "acme-request-id"] = "acme-002"
1824+
1825+
guard let request1 = try? HTTPClient.Request(url: self.defaultHTTPBinURLPrefix + "get"),
1826+
let request2 = try? HTTPClient.Request(url: self.defaultHTTPBinURLPrefix + "stats"),
1827+
let request3 = try? HTTPClient.Request(url: self.defaultHTTPBinURLPrefix + "ok") else {
1828+
XCTFail("bad stuff, can't even make request structures")
1829+
return
1830+
}
1831+
1832+
// === Request 1 (Yolo001)
1833+
XCTAssertNoThrow(try self.defaultClient.execute(request: request1,
1834+
eventLoop: .indifferent,
1835+
deadline: nil,
1836+
logger: loggerYolo001).wait())
1837+
let logsAfterReq1 = logStore.allEntries
1838+
logStore.allEntries = []
1839+
1840+
// === Request 2 (Yolo001)
1841+
XCTAssertNoThrow(try self.defaultClient.execute(request: request2,
1842+
eventLoop: .indifferent,
1843+
deadline: nil,
1844+
logger: loggerYolo001).wait())
1845+
let logsAfterReq2 = logStore.allEntries
1846+
logStore.allEntries = []
1847+
1848+
// === Request 3 (ACME002)
1849+
XCTAssertNoThrow(try self.defaultClient.execute(request: request3,
1850+
eventLoop: .indifferent,
1851+
deadline: nil,
1852+
logger: loggerACME002).wait())
1853+
let logsAfterReq3 = logStore.allEntries
1854+
logStore.allEntries = []
1855+
1856+
// === Assertions
1857+
XCTAssertGreaterThan(logsAfterReq1.count, 0)
1858+
XCTAssertGreaterThan(logsAfterReq2.count, 0)
1859+
XCTAssertGreaterThan(logsAfterReq3.count, 0)
1860+
1861+
XCTAssert(logsAfterReq1.allSatisfy { entry in
1862+
if let httpRequestMetadata = entry.metadata["ahc-request-id"],
1863+
let yoloRequestID = entry.metadata["yolo-request-id"] {
1864+
XCTAssertNil(entry.metadata["acme-request-id"])
1865+
XCTAssertEqual("yolo-001", yoloRequestID)
1866+
XCTAssertNotNil(Int(httpRequestMetadata))
1867+
return true
1868+
} else {
1869+
XCTFail("log message doesn't contain the right IDs: \(entry)")
1870+
return false
1871+
}
1872+
})
1873+
XCTAssert(logsAfterReq1.contains { entry in
1874+
entry.message == "opening fresh connection (no connections to reuse available)"
1875+
})
1876+
1877+
XCTAssert(logsAfterReq2.allSatisfy { entry in
1878+
if let httpRequestMetadata = entry.metadata["ahc-request-id"],
1879+
let yoloRequestID = entry.metadata["yolo-request-id"] {
1880+
XCTAssertNil(entry.metadata["acme-request-id"])
1881+
XCTAssertEqual("yolo-001", yoloRequestID)
1882+
XCTAssertNotNil(Int(httpRequestMetadata))
1883+
return true
1884+
} else {
1885+
XCTFail("log message doesn't contain the right IDs: \(entry)")
1886+
return false
1887+
}
1888+
})
1889+
XCTAssert(logsAfterReq2.contains { entry in
1890+
entry.message.starts(with: "leasing existing connection")
1891+
})
1892+
1893+
XCTAssert(logsAfterReq3.allSatisfy { entry in
1894+
if let httpRequestMetadata = entry.metadata["ahc-request-id"],
1895+
let acmeRequestID = entry.metadata["acme-request-id"] {
1896+
XCTAssertNil(entry.metadata["yolo-request-id"])
1897+
XCTAssertEqual("acme-002", acmeRequestID)
1898+
XCTAssertNotNil(Int(httpRequestMetadata))
1899+
return true
1900+
} else {
1901+
XCTFail("log message doesn't contain the right IDs: \(entry)")
1902+
return false
1903+
}
1904+
})
1905+
XCTAssert(logsAfterReq3.contains { entry in
1906+
entry.message.starts(with: "leasing existing connection")
1907+
})
1908+
}
1909+
1910+
func testNothingIsLoggedAtInfoOrHigher() {
1911+
let logStore = CollectEverythingLogHandler.LogStore()
1912+
1913+
var logger: Logger = Logger(label: "\(#function)", factory: { _ in
1914+
CollectEverythingLogHandler(logStore: logStore)
1915+
})
1916+
logger.logLevel = .info
1917+
1918+
guard let request1 = try? HTTPClient.Request(url: self.defaultHTTPBinURLPrefix + "get"),
1919+
let request2 = try? HTTPClient.Request(url: self.defaultHTTPBinURLPrefix + "stats") else {
1920+
XCTFail("bad stuff, can't even make request structures")
1921+
return
1922+
}
1923+
1924+
// === Request 1
1925+
XCTAssertNoThrow(try self.defaultClient.execute(request: request1,
1926+
eventLoop: .indifferent,
1927+
deadline: nil,
1928+
logger: logger).wait())
1929+
XCTAssertEqual(0, logStore.allEntries.count)
1930+
1931+
// === Request 2
1932+
XCTAssertNoThrow(try self.defaultClient.execute(request: request2,
1933+
eventLoop: .indifferent,
1934+
deadline: nil,
1935+
logger: logger).wait())
1936+
XCTAssertEqual(0, logStore.allEntries.count)
1937+
1938+
XCTAssertEqual(0, self.backgroundLogStore.allEntries.count)
1939+
}
1940+
1941+
func testAllMethodsLog() {
1942+
func checkExpectationsWithLogger<T>(type: String, _ body: (Logger, String) throws -> T) throws -> T {
1943+
let logStore = CollectEverythingLogHandler.LogStore()
1944+
1945+
var logger: Logger = Logger(label: "\(#function)", factory: { _ in
1946+
CollectEverythingLogHandler(logStore: logStore)
1947+
})
1948+
logger.logLevel = .trace
1949+
logger[metadataKey: "req"] = "yo-\(type)"
1950+
1951+
let url = self.defaultHTTPBinURLPrefix + "not-found/request/\(type))"
1952+
let result = try body(logger, url)
1953+
1954+
XCTAssertGreaterThan(logStore.allEntries.count, 0)
1955+
logStore.allEntries.forEach { entry in
1956+
XCTAssertEqual("yo-\(type)", entry.metadata["req"] ?? "n/a")
1957+
XCTAssertNotNil(Int(entry.metadata["ahc-request-id"] ?? "n/a"))
1958+
}
1959+
return result
1960+
}
1961+
1962+
XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "GET") { logger, url in
1963+
try self.defaultClient.get(url: url, logger: logger).wait()
1964+
}.status))
1965+
1966+
XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "PUT") { logger, url in
1967+
try self.defaultClient.put(url: url, logger: logger).wait()
1968+
}.status))
1969+
1970+
XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "POST") { logger, url in
1971+
try self.defaultClient.post(url: url, logger: logger).wait()
1972+
}.status))
1973+
1974+
XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "DELETE") { logger, url in
1975+
try self.defaultClient.delete(url: url, logger: logger).wait()
1976+
}.status))
1977+
1978+
XCTAssertNoThrow(XCTAssertEqual(.notFound, try checkExpectationsWithLogger(type: "PATCH") { logger, url in
1979+
try self.defaultClient.patch(url: url, logger: logger).wait()
1980+
}.status))
1981+
1982+
// No background activity expected here.
1983+
XCTAssertEqual(0, self.backgroundLogStore.allEntries.count)
1984+
}
1985+
1986+
func testClosingIdleConnectionsInPoolLogsInTheBackground() {
1987+
XCTAssertNoThrow(try self.defaultClient.get(url: self.defaultHTTPBinURLPrefix + "/get").wait())
1988+
1989+
XCTAssertNoThrow(try self.defaultClient.syncShutdown())
1990+
1991+
XCTAssertGreaterThanOrEqual(self.backgroundLogStore.allEntries.count, 0)
1992+
XCTAssert(self.backgroundLogStore.allEntries.contains { entry in
1993+
entry.message == "closing provider"
1994+
})
1995+
XCTAssert(self.backgroundLogStore.allEntries.allSatisfy { entry in
1996+
entry.metadata["ahc-request-id"] == nil &&
1997+
entry.metadata["ahc-request"] == nil &&
1998+
entry.metadata["ahc-provider"] != nil
1999+
})
2000+
2001+
self.defaultClient = nil // so it doesn't get shut down again.
2002+
}
17962003
}

‎docs/logging-design.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Design of the way AsyncHTTPClient logs
2+
3+
<details>
4+
<summary>The logging is strictly separated between request activity & background activity.</summary>
5+
AsyncHTTPClient is very much a request-driven library. Almost all work happens when you invoke a request, say `httpClient.get(someURL)`. To preserve the metadata you may have attached to your current `Logger`, we accept a `logger: Logger` parameter on each request. For example to so a `GET` request with logging use the following code.
6+
7+
```swift
8+
httpClient.get(someURL, logger: myLogger)
9+
```
10+
11+
Apart from the request-driven work, AsyncHTTPClient does do some very limited amount of background work, for example expiring connections that stayed unused in the connection pool for too long. Logs associated with the activity from background tasks can be seen only if you attach a `Logger` in `HTTPClient`'s initialiser like below.
12+
13+
```swift
14+
HTTPClient(eventLoopGroupProvider: .shared(group),
15+
backgroundActivityLogger: self.myBackgroundLogger)
16+
```
17+
18+
The rationale for the strict separation is the correct propagation of the `Logger`'s `metadata`. You are likely to attach request specific information to a `Logger` before passing it to one of AsyncHTTPClient's request methods. This metadata will then be correctly attached to all log messages that occur from AsyncHTTPClient processing this request.
19+
20+
If AsyncHTTPClient does some work in the background (like closing a connection that was long idle) however you likely do _not_ want the request-specific information from some previous request to be attached to those messages. Therefore, those messages get logged with the `backgroundActivityLogger` passed to HTTPClient's initialiser.
21+
</details>
22+
<details>
23+
<summary>Unless you explicitly pass AsyncHTTPClient a `Logger` instance, nothing is ever logged.</summary>
24+
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.
25+
</details>
26+
<details>
27+
<summary>Nothing is logged at level `info` or higher, unless something is really wrong that cannot be communicated through the API.</summary>
28+
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.
29+
</details>
30+
<details>
31+
<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>
32+
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.
33+
</details>
34+
<details>
35+
<summary>Your `Logger` metadata is preserved.</summary>
36+
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.
37+
38+
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.
39+
</details>
40+
<details>
41+
<summary>Instead of accepting one `Logger` instance per `HTTPClient` instance, each request method can accept a `Logger`.</summary>
42+
This allows AsyncHTTPClient to preserve your metadata and add its own metadata such as `ahc-request-id`.
43+
</details>
44+
<details>
45+
<summary>All logs use the [structured logging](https://www.sumologic.com/glossary/structured-logging/) pattern, i.e. only static log messages and accompanying key/value metadata are used.</summary>
46+
None of the log messages issued by AsyncHTTPClient will use String interpolation which means they will always be the exact same message.
47+
48+
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:
49+
50+
- message: `got connection for request`
51+
- metadata (the values are example):
52+
- `ahc-request-id`: `0`
53+
- `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) }`
54+
55+
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`.
56+
57+
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.
58+
</details>
59+
<details>
60+
<summary>`debug` should be enough to diagnose most problems but information that can be correlated is usually skipped.</summary>
61+
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`.
62+
63+
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.
64+
65+
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`.
66+
</details>
67+
<details>
68+
<summary>In `trace`, AsyncHTTPClient may log _a lot_.</summary>
69+
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.
70+
</details>

0 commit comments

Comments
 (0)
Please sign in to comment.