Skip to content

Commit e6ea909

Browse files
Add example of logging middleware using swift-log (#438)
### Motivation As we approach 1.0, we want more examples of how to use this package and integrate with other packages in the ecosystem. ### Modifications - Add an example that implements a logging middleware, using swift-log, `LoggingMiddlewareSwiftLog`. - Renamed the existing, OSLog-based logging middleware example to `LoggingMiddlewareOSLog`, for symmetry. - Further update the OSLog-based logging middleware to be consistent with the patterns used in the swift-log–based middleware, which was refactored to be able to implement both `ClientMiddleware` and `ServerMiddleware`. ### Result More examples. ### Test Plan - Locally verified using the following commands: - `docker-compose -f docker/docker-compose.yaml run -e SINGLE_EXAMPLE_PACKAGE=LoggingMiddlewareSwiftLog example` - `docker-compose -f docker/docker-compose.yaml run -e SINGLE_EXAMPLE_PACKAGE=LoggingMiddlewareOSLog example` - CI should build all examples.
1 parent 12cade2 commit e6ea909

File tree

18 files changed

+563
-164
lines changed

18 files changed

+563
-164
lines changed

Examples/LoggingClientMiddleware/Sources/LoggingClientMiddleware/LoggingClientMiddleware.swift

Lines changed: 0 additions & 112 deletions
This file was deleted.

Examples/LoggingClientMiddleware/Tests/LoggingClientMiddlewareTests/LoggingClientMiddlewareTests.swift

Lines changed: 0 additions & 39 deletions
This file was deleted.

Examples/LoggingClientMiddleware/Package.swift renamed to Examples/LoggingMiddlewareOSLog/Package.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import PackageDescription
1616

1717
let package = Package(
18-
name: "LoggingClientMiddleware",
18+
name: "LoggingMiddlewareOSLog",
1919
platforms: [.macOS(.v11), .iOS(.v14), .tvOS(.v14), .watchOS(.v7), .visionOS(.v1)],
2020
dependencies: [
2121
.package(url: "https://github.com/apple/swift-openapi-generator", exact: "1.0.0-alpha.1"),
@@ -25,7 +25,7 @@ let package = Package(
2525
],
2626
targets: [
2727
.target(
28-
name: "LoggingClientMiddleware",
28+
name: "LoggingMiddleware",
2929
dependencies: [
3030
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
3131
.product(name: "HTTPTypes", package: "swift-http-types"),
@@ -34,10 +34,10 @@ let package = Package(
3434
.executableTarget(
3535
name: "HelloWorldURLSessionClient",
3636
dependencies: [
37-
"LoggingClientMiddleware", .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
37+
"LoggingMiddleware", .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
3838
.product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"),
3939
],
4040
plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")]
41-
), .testTarget(name: "LoggingClientMiddlewareTests", dependencies: ["LoggingClientMiddleware"]),
41+
), .testTarget(name: "LoggingClientMiddlewareTests", dependencies: ["LoggingMiddleware"]),
4242
]
4343
)

Examples/LoggingClientMiddleware/README.md renamed to Examples/LoggingMiddlewareOSLog/README.md

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ requests and responses.
66
## Overview
77

88
This example extends the [HelloWorldURLSessionClient](../HelloWorldURLSessionClient)
9-
with a new target, `LoggingClientMiddleware`, which is then used when creating
9+
with a new target, `LoggingMiddleware`, which is then used when creating
1010
the `Client`.
1111

1212
Because request and response bodies support streaming and can be arbitrarily
@@ -29,22 +29,36 @@ chain.
2929

3030
## Testing
3131

32-
Run the client executable using:
32+
This example implementation is logging requests and response at debug level
33+
using `com.apple.swift-openapi` as the subsystem. By default, debug logs are
34+
only captured in memory and not persisted unless a configuration change is made
35+
using `log config`. Rather than make a system-wide change, to show this
36+
middleware in action we'll use `log stream` (cf. `log show`). In one terminal,
37+
run the following command (it will not return until you interrupt it using
38+
CTRL-C):
3339

3440
```console
35-
swift run
41+
% log stream --debug --info --style compact --predicate subsystem == 'com.apple.swift-openapi'
42+
Filtering the log data using "subsystem == "com.apple.swift-openapi""
43+
```
44+
45+
In another terminal, run the client executable using:
46+
47+
```console
48+
% swift run
3649
Hello, Stranger!
3750
```
3851

39-
Check the system logs for logs in the last 5 minutes with the subsystem used
40-
by the middleware:
52+
You should see in the terminal running `log stream`, that the logs have been
53+
displayed:
4154

4255
```console
43-
% log show --last 5m --style compact --debug --info --predicate "subsystem == 'com.apple.swift-openapi'"
56+
% log stream --debug --info --style compact --predicate subsystem == 'com.apple.swift-openapi'
4457
Filtering the log data using "subsystem == "com.apple.swift-openapi""
4558
Timestamp Ty Process[PID:TID]
46-
2023-12-06 20:12:41.758 Db HelloWorldURLSessionClient[63324:baf40a6] [com.apple.swift-openapi:logging-middleware] Request: GET /greet body: <none>
47-
2023-12-06 20:12:41.954 Db HelloWorldURLSessionClient[63324:baf40a9] [com.apple.swift-openapi:logging-middleware] Response: GET /greet 200 body: {
59+
2023-12-07 17:09:20.256 Db HelloWorldURLSessionClient[32556:bdad678] [com.apple.swift-openapi:logging-middleware] Request: GET /greet body: <none>
60+
2023-12-07 17:09:20.429 Db HelloWorldURLSessionClient[32556:bdad67a] [com.apple.swift-openapi:logging-middleware] Response: GET /greet 200 body: {
4861
"message" : "Hello, Stranger!"
4962
}
63+
^C
5064
```
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
//===----------------------------------------------------------------------===//
1414
import OpenAPIURLSession
1515
import Foundation
16-
import LoggingClientMiddleware
16+
import LoggingMiddleware
1717

1818
@main struct HelloWorldURLSessionClient {
1919
static func main() async throws {
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator 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 SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
#if canImport(Darwin)
15+
import OpenAPIRuntime
16+
import Foundation
17+
import HTTPTypes
18+
import OSLog
19+
20+
package actor LoggingMiddleware {
21+
private let logger: Logger
22+
package let bodyLoggingPolicy: BodyLoggingPolicy
23+
24+
package init(logger: Logger = defaultLogger, bodyLoggingConfiguration: BodyLoggingPolicy = .never) {
25+
self.logger = logger
26+
self.bodyLoggingPolicy = bodyLoggingConfiguration
27+
}
28+
29+
fileprivate static var defaultLogger: Logger {
30+
Logger(subsystem: "com.apple.swift-openapi", category: "logging-middleware")
31+
}
32+
}
33+
34+
extension LoggingMiddleware: ClientMiddleware {
35+
package func intercept(
36+
_ request: HTTPRequest,
37+
body: HTTPBody?,
38+
baseURL: URL,
39+
operationID: String,
40+
next: (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
41+
) async throws -> (HTTPResponse, HTTPBody?) {
42+
let (requestBodyToLog, requestBodyForNext) = try await bodyLoggingPolicy.process(body)
43+
log(request, requestBodyToLog)
44+
do {
45+
let (response, responseBody) = try await next(request, requestBodyForNext, baseURL)
46+
let (responseBodyToLog, responseBodyForNext) = try await bodyLoggingPolicy.process(responseBody)
47+
log(request, response, responseBodyToLog)
48+
return (response, responseBodyForNext)
49+
} catch {
50+
log(request, failedWith: error)
51+
throw error
52+
}
53+
}
54+
}
55+
56+
extension LoggingMiddleware: ServerMiddleware {
57+
package func intercept(
58+
_ request: HTTPTypes.HTTPRequest,
59+
body: OpenAPIRuntime.HTTPBody?,
60+
metadata: OpenAPIRuntime.ServerRequestMetadata,
61+
operationID: String,
62+
next: @Sendable (HTTPTypes.HTTPRequest, OpenAPIRuntime.HTTPBody?, OpenAPIRuntime.ServerRequestMetadata)
63+
async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?)
64+
) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) {
65+
let (requestBodyToLog, requestBodyForNext) = try await bodyLoggingPolicy.process(body)
66+
log(request, requestBodyToLog)
67+
do {
68+
let (response, responseBody) = try await next(request, requestBodyForNext, metadata)
69+
let (responseBodyToLog, responseBodyForNext) = try await bodyLoggingPolicy.process(responseBody)
70+
log(request, response, responseBodyToLog)
71+
return (response, responseBodyForNext)
72+
} catch {
73+
log(request, failedWith: error)
74+
throw error
75+
}
76+
}
77+
}
78+
79+
extension LoggingMiddleware {
80+
func log(_ request: HTTPRequest, _ requestBody: BodyLoggingPolicy.BodyLog) {
81+
logger.debug(
82+
"Request: \(request.method, privacy: .public) \(request.path ?? "<nil>", privacy: .public) body: \(requestBody, privacy: .auto)"
83+
)
84+
}
85+
86+
func log(_ request: HTTPRequest, _ response: HTTPResponse, _ responseBody: BodyLoggingPolicy.BodyLog) {
87+
logger.debug(
88+
"Response: \(request.method, privacy: .public) \(request.path ?? "<nil>", privacy: .public) \(response.status, privacy: .public) body: \(responseBody, privacy: .auto)"
89+
)
90+
}
91+
92+
func log(_ request: HTTPRequest, failedWith error: any Error) {
93+
logger.warning("Request failed. Error: \(error.localizedDescription)")
94+
}
95+
}
96+
97+
package enum BodyLoggingPolicy {
98+
/// Never log request or response bodies.
99+
case never
100+
/// Log request and response bodies that have a known length less than or equal to `maxBytes`.
101+
case upTo(maxBytes: Int)
102+
103+
enum BodyLog: Equatable, CustomStringConvertible {
104+
/// There is no body to log.
105+
case none
106+
/// The policy forbids logging the body.
107+
case redacted
108+
/// The body was of unknown length.
109+
case unknownLength
110+
/// The body exceeds the maximum size for logging allowed by the policy.
111+
case tooManyBytesToLog(Int64)
112+
/// The body can be logged.
113+
case complete(Data)
114+
115+
var description: String {
116+
switch self {
117+
case .none: return "<none>"
118+
case .redacted: return "<redacted>"
119+
case .unknownLength: return "<unknown length>"
120+
case .tooManyBytesToLog(let byteCount): return "<\(byteCount) bytes>"
121+
case .complete(let data):
122+
if let string = String(data: data, encoding: .utf8) { return string }
123+
return String(describing: data)
124+
}
125+
}
126+
}
127+
128+
func process(_ body: HTTPBody?) async throws -> (bodyToLog: BodyLog, bodyForNext: HTTPBody?) {
129+
switch (body?.length, self) {
130+
case (.none, _): return (.none, body)
131+
case (_, .never): return (.redacted, body)
132+
case (.unknown, _): return (.unknownLength, body)
133+
case (.known(let length), .upTo(let maxBytesToLog)) where length > maxBytesToLog:
134+
return (.tooManyBytesToLog(length), body)
135+
case (.known, .upTo(let maxBytesToLog)):
136+
let bodyData = try await Data(collecting: body!, upTo: maxBytesToLog)
137+
return (.complete(bodyData), HTTPBody(bodyData))
138+
}
139+
}
140+
}
141+
#endif // canImport(Darwin)

0 commit comments

Comments
 (0)