Skip to content

Commit 23e6592

Browse files
Add example of tracing middleware with swift-distributed-tracing (#449)
### 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 uses swift-distributed-tracing and swift-otel to collect and emit traces. ### Result More examples. ### Test Plan ```console % docker-compose -f docker/docker-compose.yaml run -e SINGLE_EXAMPLE_PACKAGE=TracingMiddleware examples ... ** Copying example TracingMiddleware to /tmp/test-examples.sh.QKVmwEe2Pl/TracingMiddleware ... ** ✅ Successfully built the example package TracingMiddleware. ``` Signed-off-by: Si Beaumont <[email protected]>
1 parent c56d0dd commit 23e6592

File tree

9 files changed

+349
-0
lines changed

9 files changed

+349
-0
lines changed

Examples/TracingMiddleware/.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.DS_Store
2+
.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/
6+
DerivedData/
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.vscode
9+
/Package.resolved
10+
.ci/
11+
.docc-build/
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// swift-tools-version:5.9
2+
//===----------------------------------------------------------------------===//
3+
//
4+
// This source file is part of the SwiftOpenAPIGenerator open source project
5+
//
6+
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
7+
// Licensed under Apache License v2.0
8+
//
9+
// See LICENSE.txt for license information
10+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
11+
//
12+
// SPDX-License-Identifier: Apache-2.0
13+
//
14+
//===----------------------------------------------------------------------===//
15+
import PackageDescription
16+
17+
let package = Package(
18+
name: "TracingMiddleware",
19+
platforms: [.macOS(.v13)],
20+
dependencies: [
21+
.package(url: "https://github.com/apple/swift-openapi-generator", exact: "1.0.0-alpha.1"),
22+
.package(url: "https://github.com/apple/swift-openapi-runtime", exact: "1.0.0-alpha.1"),
23+
.package(url: "https://github.com/swift-server/swift-openapi-vapor", exact: "1.0.0-alpha.1"),
24+
.package(url: "https://github.com/vapor/vapor", from: "4.87.1"),
25+
.package(url: "https://github.com/apple/swift-distributed-tracing", from: "1.0.1"),
26+
.package(url: "https://github.com/apple/swift-distributed-tracing-extras", exact: "1.0.0-beta.1"),
27+
.package(url: "https://github.com/apple/swift-nio", from: "2.62.0"),
28+
.package(url: "https://github.com/slashmo/swift-otel", .upToNextMinor(from: "0.8.0")),
29+
],
30+
targets: [
31+
.target(
32+
name: "TracingMiddleware",
33+
dependencies: [
34+
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
35+
.product(name: "Tracing", package: "swift-distributed-tracing"),
36+
.product(name: "TracingOpenTelemetrySemanticConventions", package: "swift-distributed-tracing-extras"),
37+
]
38+
),
39+
.executableTarget(
40+
name: "HelloWorldVaporServer",
41+
dependencies: [
42+
"TracingMiddleware", .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
43+
.product(name: "OpenAPIVapor", package: "swift-openapi-vapor"),
44+
.product(name: "Vapor", package: "vapor"), .product(name: "NIO", package: "swift-nio"),
45+
.product(name: "OpenTelemetry", package: "swift-otel"),
46+
.product(name: "OtlpGRPCSpanExporting", package: "swift-otel"),
47+
],
48+
plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")]
49+
),
50+
]
51+
)

Examples/TracingMiddleware/README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Tracing middleware using Swift OTel
2+
3+
In this example we'll implement a `ClientMiddleware` and `ServerMiddleware`
4+
that use `swift-otel` to emit traces for requests and responses.
5+
6+
## Overview
7+
8+
This example extends the [HelloWorldVaporServer](../HelloWorldVaporServer)
9+
with a new target, `TracingMiddleware`, which is then used when creating
10+
the `Server`.
11+
12+
## Testing
13+
14+
### Running the collector and visualization containers
15+
16+
We'll use [Compose](https://docs.docker.com/compose) to run a set of containers
17+
to collect and visualize the traces. In one terminal window, run the following
18+
command:
19+
20+
```console
21+
% docker compose -f docker/docker-compose.yaml up
22+
[+] Running 4/4
23+
⠿ Network tracingmiddleware_exporter Created 0.1s
24+
⠿ Container tracingmiddleware-jaeger-1 Created 0.3s
25+
⠿ Container tracingmiddleware-zipkin-1 Created 0.4s
26+
⠿ Container tracingmiddleware-otel-collector-1 Created 0.2s
27+
...
28+
```
29+
30+
At this point the tracing collector and visualization tools are running.
31+
32+
### Running the server
33+
34+
Now, in another terminal, run the server locally using the following command:
35+
36+
```console
37+
% swift run
38+
```
39+
40+
### Making some requests
41+
42+
Finally, in a third terminal, make a few requests to the server:
43+
44+
```console
45+
% xargs -n1 -I% curl "localhost:8080/api/greet?name=%" <<< "Juan Mei Tom Bill Anne Ravi Maria"
46+
{
47+
"message" : "Hello, Juan!"
48+
}
49+
{
50+
"message" : "Hello, Mei!"
51+
}
52+
{
53+
"message" : "Hello, Tom!"
54+
}
55+
{
56+
"message" : "Hello, Bill!"
57+
}
58+
{
59+
"message" : "Hello, Anne!"
60+
}
61+
{
62+
"message" : "Hello, Ravi!"
63+
}
64+
{
65+
"message" : "Hello, Maria!"
66+
}
67+
```
68+
69+
### Visualizing the traces using Jaeger UI
70+
71+
Visit Jaeger UI in your browser at [localhost:16686](http://localhost:16686).
72+
73+
Select `HelloWorldServer` from the dropdown and click `Find Traces`, or use
74+
[this pre-canned link](http://localhost:16686/search?service=HelloWorldServer).
75+
76+
See the traces for the recent requests and click to select a trace for a given request.
77+
78+
Click to expand the trace, the metadata associated with the request and the
79+
process, and the events.
80+
81+
### Visualizing the traces using Zipkin
82+
83+
Now visit Zipkin in your browser at [localhost:9411](http://localhost:9411).
84+
85+
Click to run the empty query and then select a trace.
86+
87+
Similar to Jaeger, you can inspect the trace, the metadata associated with the
88+
request, and the events.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
import OpenAPIRuntime
15+
import OpenAPIVapor
16+
import Vapor
17+
import TracingMiddleware
18+
import Tracing
19+
import OpenTelemetry
20+
import OtlpGRPCSpanExporting
21+
import NIO
22+
23+
struct Handler: APIProtocol {
24+
func getGreeting(_ input: Operations.getGreeting.Input) async throws -> Operations.getGreeting.Output {
25+
let name = input.query.name ?? "Stranger"
26+
return .ok(.init(body: .json(.init(message: "Hello, \(name)!"))))
27+
}
28+
}
29+
30+
@main struct HelloWorldVaporServer {
31+
static func main() throws {
32+
let eventLoopGroup = MultiThreadedEventLoopGroup.singleton
33+
let otel = OTel(
34+
serviceName: "HelloWorldServer",
35+
eventLoopGroup: eventLoopGroup,
36+
processor: OTel.BatchSpanProcessor(
37+
exportingTo: OtlpGRPCSpanExporter(config: .init(eventLoopGroup: eventLoopGroup)),
38+
eventLoopGroup: eventLoopGroup
39+
)
40+
)
41+
try otel.start().wait()
42+
defer { try? otel.shutdown().wait() }
43+
InstrumentationSystem.bootstrap(otel.tracer())
44+
45+
let app = Vapor.Application()
46+
let transport = VaporTransport(routesBuilder: app)
47+
let handler = Handler()
48+
try handler.registerHandlers(on: transport, serverURL: URL(string: "/api")!, middlewares: [TracingMiddleware()])
49+
try app.run()
50+
}
51+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
generate:
2+
- types
3+
- server
4+
accessModifier: internal
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
openapi: '3.1.0'
2+
info:
3+
title: GreetingService
4+
version: 1.0.0
5+
servers:
6+
- url: https://example.com/api
7+
description: Example service deployment.
8+
paths:
9+
/greet:
10+
get:
11+
operationId: getGreeting
12+
parameters:
13+
- name: name
14+
required: false
15+
in: query
16+
description: The name used in the returned greeting.
17+
schema:
18+
type: string
19+
responses:
20+
'200':
21+
description: A success response with a greeting.
22+
content:
23+
application/json:
24+
schema:
25+
$ref: '#/components/schemas/Greeting'
26+
components:
27+
schemas:
28+
Greeting:
29+
type: object
30+
description: A value with the greeting contents.
31+
properties:
32+
message:
33+
type: string
34+
description: The string representation of the greeting.
35+
required:
36+
- message
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
import Foundation
15+
import HTTPTypes
16+
import OpenAPIRuntime
17+
import Tracing
18+
import TracingOpenTelemetrySemanticConventions
19+
20+
package actor TracingMiddleware { package init() {} }
21+
22+
extension TracingMiddleware: ClientMiddleware {
23+
package func intercept(
24+
_ request: HTTPRequest,
25+
body: HTTPBody?,
26+
baseURL: URL,
27+
operationID: String,
28+
next: (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
29+
) async throws -> (HTTPResponse, HTTPBody?) {
30+
try await withSpan(operationID, ofKind: .client) { span in
31+
span.addEvent("Sending request")
32+
span.attributes.http.method = request.method.rawValue
33+
span.attributes.http.target = request.path
34+
let (response, responseBody) = try await next(request, body, baseURL)
35+
span.attributes.http.statusCode = response.status.code
36+
span.addEvent("Received response")
37+
return (response, responseBody)
38+
}
39+
}
40+
}
41+
42+
extension TracingMiddleware: ServerMiddleware {
43+
package func intercept(
44+
_ request: HTTPRequest,
45+
body: HTTPBody?,
46+
metadata: ServerRequestMetadata,
47+
operationID: String,
48+
next: (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody?)
49+
) async throws -> (HTTPResponse, HTTPBody?) {
50+
try await withSpan(operationID, ofKind: .server) { span in
51+
span.addEvent("Received request")
52+
span.attributes.http.method = request.method.rawValue
53+
span.attributes.http.target = request.path
54+
let (response, responseBody) = try await next(request, body, metadata)
55+
span.attributes.http.statusCode = response.status.code
56+
span.addEvent("Sending response")
57+
return (response, responseBody)
58+
}
59+
}
60+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
receivers:
2+
otlp:
3+
protocols:
4+
grpc:
5+
endpoint: otel-collector:4317
6+
7+
exporters:
8+
logging:
9+
verbosity: detailed
10+
jaeger:
11+
endpoint: "jaeger:14250"
12+
tls:
13+
insecure: true
14+
15+
zipkin:
16+
endpoint: "http://zipkin:9411/api/v2/spans"
17+
18+
service:
19+
pipelines:
20+
traces:
21+
receivers: otlp
22+
exporters: [logging, jaeger, zipkin]
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
version: '3'
2+
services:
3+
otel-collector:
4+
image: otel/opentelemetry-collector-contrib:latest
5+
command: ["--config=/etc/config.yaml"]
6+
volumes:
7+
- ./collector-config.yaml:/etc/config.yaml
8+
ports:
9+
- "4317:4317"
10+
networks: [exporter]
11+
depends_on: [zipkin, jaeger]
12+
13+
jaeger:
14+
image: jaegertracing/all-in-one
15+
ports:
16+
- "16686:16686"
17+
networks: [exporter]
18+
19+
zipkin:
20+
image: openzipkin/zipkin:latest
21+
ports:
22+
- "9411:9411"
23+
networks: [exporter]
24+
25+
networks:
26+
exporter:

0 commit comments

Comments
 (0)