Skip to content

Commit d27c298

Browse files
authored
Capture logs in tests (#903)
Motivation: In #902 we changed behaviour such that gRPC will not log unless you explicitly pass in a logger. When tests fail it's useful to have the logs to triage them, especially when they fail under CI, for example. Modifications: - Add a capturing log handler factory to the test module which captures all logs emitted by handlers it makes - Wire up loggers in tests - Hook up `GRPCTestCase` to print the captured logs only if a test fails (or an environment variable was set) - Add a bunch of missing `super.setUp` and `super.tearDown` calls Result: - If a test fails we get all `.trace` level logs emitted during that test
1 parent d4633f2 commit d27c298

22 files changed

+271
-75
lines changed

Tests/GRPCTests/AnyServiceClientTests.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import XCTest
2020

2121
class AnyServiceClientTests: EchoTestCaseBase {
2222
var anyServiceClient: AnyServiceClient {
23-
return AnyServiceClient(channel: self.client.channel)
23+
return AnyServiceClient(
24+
channel: self.client.channel,
25+
defaultCallOptions: self.callOptionsWithLogger
26+
)
2427
}
2528

2629
func testUnary() throws {

Tests/GRPCTests/BasicEchoTestCase.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,23 +100,26 @@ class EchoTestCaseBase: GRPCTestCase {
100100
return try self.serverBuilder()
101101
.withErrorDelegate(makeErrorDelegate())
102102
.withServiceProviders([makeEchoProvider()])
103+
.withLogger(self.serverLogger)
103104
.bind(host: "localhost", port: 0)
104105
.wait()
105106
}
106107

107108
func makeClientConnection(port: Int) throws -> ClientConnection {
108-
return self.connectionBuilder().connect(
109-
host: "localhost",
110-
port: port
111-
)
109+
return self.connectionBuilder()
110+
.withBackgroundActivityLogger(self.clientLogger)
111+
.connect(host: "localhost", port: port)
112112
}
113113

114114
func makeEchoProvider() -> Echo_EchoProvider { return EchoProvider() }
115115

116116
func makeErrorDelegate() -> ServerErrorDelegate? { return nil }
117117

118118
func makeEchoClient(port: Int) throws -> Echo_EchoClient {
119-
return Echo_EchoClient(channel: try self.makeClientConnection(port: port))
119+
return Echo_EchoClient(
120+
channel: try self.makeClientConnection(port: port),
121+
defaultCallOptions: self.callOptionsWithLogger
122+
)
120123
}
121124

122125
override func setUp() {

Tests/GRPCTests/CallStartBehaviorTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,13 @@ class CallStartBehaviorTests: GRPCTestCase {
2929
// and the RPC wouldn't complete until we call shutdown (because we're not setting a timeout).
3030
let channel = ClientConnection.insecure(group: group)
3131
.withCallStartBehavior(.fastFailure)
32+
.withBackgroundActivityLogger(self.clientLogger)
3233
.connect(host: "http://unreachable.invalid", port: 0)
3334
defer {
3435
XCTAssertNoThrow(try channel.close().wait())
3536
}
3637

37-
let echo = Echo_EchoClient(channel: channel)
38+
let echo = Echo_EchoClient(channel: channel, defaultCallOptions: self.callOptionsWithLogger)
3839
let get = echo.get(.with { $0.text = "Is anyone out there?" })
3940

4041
XCTAssertThrowsError(try get.response.wait())
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2020, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import struct Foundation.Date
17+
import Logging
18+
import NIOConcurrencyHelpers
19+
20+
/// A `LogHandler` factory which captures all logs emitted by the handlers it makes.
21+
internal class CapturingLogHandlerFactory {
22+
private var lock = Lock()
23+
private var _logs: [CapturedLog] = []
24+
25+
/// Returns all captured logs and empties the store of captured logs.
26+
func clearCapturedLogs() -> [CapturedLog] {
27+
return self.lock.withLock {
28+
let logs = self._logs
29+
self._logs.removeAll()
30+
return logs
31+
}
32+
}
33+
34+
/// Make a `LogHandler` whose logs will be recorded by this factory.
35+
func make(_ label: String) -> LogHandler {
36+
return CapturingLogHandler(label: label) { log in
37+
self.lock.withLockVoid {
38+
self._logs.append(log)
39+
}
40+
}
41+
}
42+
}
43+
44+
/// A captured log.
45+
internal struct CapturedLog {
46+
var label: String
47+
var level: Logger.Level
48+
var message: Logger.Message
49+
var metadata: Logger.Metadata
50+
var file: String
51+
var function: String
52+
var line: UInt
53+
var date: Date
54+
}
55+
56+
/// A log handler which captures all logs it records.
57+
internal struct CapturingLogHandler: LogHandler {
58+
private let capture: (CapturedLog) -> Void
59+
60+
internal let label: String
61+
internal var metadata: Logger.Metadata = [:]
62+
internal var logLevel: Logger.Level = .trace
63+
64+
fileprivate init(label: String, capture: @escaping (CapturedLog) -> ()) {
65+
self.label = label
66+
self.capture = capture
67+
}
68+
69+
internal func log(
70+
level: Logger.Level,
71+
message: Logger.Message,
72+
metadata: Logger.Metadata?,
73+
file: String,
74+
function: String,
75+
line: UInt
76+
) {
77+
let merged: Logger.Metadata
78+
79+
if let metadata = metadata {
80+
merged = self.metadata.merging(metadata, uniquingKeysWith: { old, new in return new })
81+
} else {
82+
merged = self.metadata
83+
}
84+
85+
let log = CapturedLog(
86+
label: self.label,
87+
level: level,
88+
message: message,
89+
metadata: merged,
90+
file: file,
91+
function: function,
92+
line: line,
93+
date: Date()
94+
)
95+
96+
self.capture(log)
97+
}
98+
99+
internal subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
100+
get {
101+
return self.metadata[metadataKey]
102+
}
103+
set {
104+
self.metadata[metadataKey] = newValue
105+
}
106+
}
107+
}

Tests/GRPCTests/ClientConnectionBackoffTests.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class ClientConnectionBackoffTests: GRPCTestCase {
3333
var connectionStateRecorder = RecordingConnectivityDelegate()
3434

3535
override func setUp() {
36+
super.setUp()
3637
self.serverGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
3738
self.clientGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
3839
}
@@ -55,11 +56,14 @@ class ClientConnectionBackoffTests: GRPCTestCase {
5556
XCTAssertNoThrow(try self.clientGroup.syncShutdownGracefully())
5657
self.client = nil
5758
self.clientGroup = nil
59+
60+
super.tearDown()
5861
}
5962

6063
func makeServer() -> EventLoopFuture<Server> {
6164
return Server.insecure(group: self.serverGroup)
6265
.withServiceProviders([EchoProvider()])
66+
.withLogger(self.serverLogger)
6367
.bind(host: "localhost", port: self.port)
6468
}
6569

@@ -68,6 +72,7 @@ class ClientConnectionBackoffTests: GRPCTestCase {
6872
.withConnectivityStateDelegate(self.connectionStateRecorder)
6973
.withConnectionBackoff(maximum: .milliseconds(100))
7074
.withConnectionTimeout(minimum: .milliseconds(100))
75+
.withBackgroundActivityLogger(self.clientLogger)
7176
}
7277

7378
func testClientConnectionFailsWithNoBackoff() throws {
@@ -83,7 +88,7 @@ class ClientConnectionBackoffTests: GRPCTestCase {
8388
.connect(host: "localhost", port: self.port)
8489

8590
// Start an RPC to trigger creating a channel.
86-
let echo = Echo_EchoClient(channel: self.client)
91+
let echo = Echo_EchoClient(channel: self.client, defaultCallOptions: self.callOptionsWithLogger)
8792
_ = echo.get(.with { $0.text = "foo" })
8893

8994
self.connectionStateRecorder.waitForExpectedChanges(timeout: .seconds(5))
@@ -104,7 +109,7 @@ class ClientConnectionBackoffTests: GRPCTestCase {
104109
.connect(host: "localhost", port: self.port)
105110

106111
// Start an RPC to trigger creating a channel.
107-
let echo = Echo_EchoClient(channel: self.client)
112+
let echo = Echo_EchoClient(channel: self.client, defaultCallOptions: self.callOptionsWithLogger)
108113
_ = echo.get(.with { $0.text = "foo" })
109114

110115
self.connectionStateRecorder.waitForExpectedChanges(timeout: .seconds(5))
@@ -123,7 +128,7 @@ class ClientConnectionBackoffTests: GRPCTestCase {
123128
.connect(host: "localhost", port: self.port)
124129

125130
// Start an RPC to trigger creating a channel.
126-
let echo = Echo_EchoClient(channel: self.client)
131+
let echo = Echo_EchoClient(channel: self.client, defaultCallOptions: self.callOptionsWithLogger)
127132
_ = echo.get(.with { $0.text = "foo" })
128133

129134
self.connectionStateRecorder.waitForExpectedChanges(timeout: .seconds(5))
@@ -162,7 +167,7 @@ class ClientConnectionBackoffTests: GRPCTestCase {
162167
.connect(host: "localhost", port: self.port)
163168

164169
// Start an RPC to trigger creating a channel.
165-
let echo = Echo_EchoClient(channel: self.client)
170+
let echo = Echo_EchoClient(channel: self.client, defaultCallOptions: self.callOptionsWithLogger)
166171
_ = echo.get(.with { $0.text = "foo" })
167172

168173
// Wait for the connection to be ready.

Tests/GRPCTests/ClientTLSFailureTests.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ class ClientTLSFailureTests: GRPCTestCase {
6262
eventLoopGroup: self.clientEventLoopGroup,
6363
tls: tls,
6464
// No need to retry connecting.
65-
connectionBackoff: nil
65+
connectionBackoff: nil,
66+
backgroundActivityLogger: self.clientLogger
6667
)
6768
}
6869

@@ -71,13 +72,16 @@ class ClientTLSFailureTests: GRPCTestCase {
7172
}
7273

7374
override func setUp() {
75+
super.setUp()
76+
7477
self.serverEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
7578

7679
self.server = try! Server.secure(
7780
group: self.serverEventLoopGroup,
7881
certificateChain: [SampleCertificate.server.certificate],
7982
privateKey: SamplePrivateKey.server
8083
).withServiceProviders([EchoProvider()])
84+
.withLogger(self.serverLogger)
8185
.bind(host: "localhost", port: 0)
8286
.wait()
8387

@@ -97,6 +101,8 @@ class ClientTLSFailureTests: GRPCTestCase {
97101
XCTAssertNoThrow(try self.serverEventLoopGroup.syncShutdownGracefully())
98102
self.server = nil
99103
self.serverEventLoopGroup = nil
104+
105+
super.tearDown()
100106
}
101107

102108
func testClientConnectionFailsWhenServerIsUnknown() throws {

Tests/GRPCTests/ClientTLSTests.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,14 @@ class ClientTLSHostnameOverrideTests: GRPCTestCase {
3333
}
3434

3535
override func tearDown() {
36-
super.tearDown()
3736
XCTAssertNoThrow(try self.server.close().wait())
3837
XCTAssertNoThrow(try connection.close().wait())
3938
XCTAssertNoThrow(try self.eventLoopGroup.syncShutdownGracefully())
39+
super.tearDown()
4040
}
4141

42-
4342
func doTestUnary() throws {
44-
let client = Echo_EchoClient(channel: self.connection)
43+
let client = Echo_EchoClient(channel: self.connection, defaultCallOptions: self.callOptionsWithLogger)
4544
let get = client.get(.with { $0.text = "foo" })
4645

4746
let response = try get.response.wait()
@@ -59,6 +58,7 @@ class ClientTLSHostnameOverrideTests: GRPCTestCase {
5958
self.server = try Server.secure(group: self.eventLoopGroup, certificateChain: [cert], privateKey: key)
6059
.withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate]))
6160
.withServiceProviders([EchoProvider()])
61+
.withLogger(self.serverLogger)
6262
.bind(host: "localhost", port: 0)
6363
.wait()
6464

@@ -70,6 +70,7 @@ class ClientTLSHostnameOverrideTests: GRPCTestCase {
7070
self.connection = ClientConnection.secure(group: self.eventLoopGroup)
7171
.withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate]))
7272
.withTLS(serverHostnameOverride: "example.com")
73+
.withBackgroundActivityLogger(self.clientLogger)
7374
.connect(host: "localhost", port: port)
7475

7576
try self.doTestUnary()
@@ -83,6 +84,7 @@ class ClientTLSHostnameOverrideTests: GRPCTestCase {
8384
self.server = try Server.secure(group: self.eventLoopGroup, certificateChain: [cert], privateKey: key)
8485
.withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate]))
8586
.withServiceProviders([EchoProvider()])
87+
.withLogger(self.serverLogger)
8688
.bind(host: "localhost", port: 0)
8789
.wait()
8890

@@ -93,6 +95,7 @@ class ClientTLSHostnameOverrideTests: GRPCTestCase {
9395

9496
self.connection = ClientConnection.secure(group: self.eventLoopGroup)
9597
.withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate]))
98+
.withBackgroundActivityLogger(self.clientLogger)
9699
.connect(host: "localhost", port: port)
97100

98101
try self.doTestUnary()

Tests/GRPCTests/ClientTimeoutTests.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ class ClientTimeoutTests: GRPCTestCase {
2828
// We use a deadline here because internally we convert timeouts into deadlines by diffing
2929
// with `DispatchTime.now()`. We therefore need the deadline to be known in advance. Note we
3030
// use zero because `EmbeddedEventLoop`s time starts at zero.
31-
return CallOptions(timeLimit: .deadline(.uptimeNanoseconds(0) + timeout))
31+
var options = self.callOptionsWithLogger
32+
options.timeLimit = .deadline(.uptimeNanoseconds(0) + timeout)
33+
return options
3234
}
3335

3436
// Note: this is not related to the call timeout since we're using an EmbeddedChannel. We require
@@ -38,7 +40,7 @@ class ClientTimeoutTests: GRPCTestCase {
3840
override func setUp() {
3941
super.setUp()
4042

41-
let connection = EmbeddedGRPCChannel()
43+
let connection = EmbeddedGRPCChannel(logger: self.clientLogger)
4244
XCTAssertNoThrow(try connection.embeddedChannel.connect(to: SocketAddress(unixDomainSocketPath: "/foo")))
4345
let client = Echo_EchoClient(channel: connection, defaultCallOptions: self.callOptions)
4446

@@ -48,6 +50,7 @@ class ClientTimeoutTests: GRPCTestCase {
4850

4951
override func tearDown() {
5052
XCTAssertNoThrow(try self.channel.finish())
53+
super.tearDown()
5154
}
5255

5356
func assertRPCTimedOut(_ response: EventLoopFuture<Echo_EchoResponse>, expectation: XCTestExpectation) {

Tests/GRPCTests/CompressionTests.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,30 +29,34 @@ class MessageCompressionTests: GRPCTestCase {
2929
var echo: Echo_EchoClient!
3030

3131
override func setUp() {
32+
super.setUp()
3233
self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
3334
}
3435

3536
override func tearDown() {
3637
XCTAssertNoThrow(try self.client.close().wait())
3738
XCTAssertNoThrow(try self.server.close().wait())
3839
XCTAssertNoThrow(try self.group.syncShutdownGracefully())
40+
super.tearDown()
3941
}
4042

4143
func setupServer(encoding: ServerMessageEncoding) throws {
4244
self.server = try Server.insecure(group: self.group)
4345
.withServiceProviders([EchoProvider()])
4446
.withMessageCompression(encoding)
47+
.withLogger(self.serverLogger)
4548
.bind(host: "localhost", port: 0)
4649
.wait()
4750
}
4851

4952
func setupClient(encoding: ClientMessageEncoding) {
5053
self.client = ClientConnection.insecure(group: self.group)
54+
.withBackgroundActivityLogger(self.clientLogger)
5155
.connect(host: "localhost", port: self.server.channel.localAddress!.port!)
5256

5357
self.echo = Echo_EchoClient(
5458
channel: self.client,
55-
defaultCallOptions: CallOptions(messageEncoding: encoding)
59+
defaultCallOptions: CallOptions(messageEncoding: encoding, logger: self.clientLogger)
5660
)
5761
}
5862

Tests/GRPCTests/ConnectionManagerTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ class ConnectionManagerTests: GRPCTestCase {
2727
target: .unixDomainSocket("/ignored"),
2828
eventLoopGroup: self.loop,
2929
connectivityStateDelegate: self.recorder,
30-
connectionBackoff: nil
30+
connectionBackoff: nil,
31+
backgroundActivityLogger: self.clientLogger
3132
)
3233
}
3334

0 commit comments

Comments
 (0)