Skip to content

Add Distributed Tracing support #177

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.DS_Store
/.build
.build
.swiftpm
/Packages
xcuserdata/
DerivedData/
Expand All @@ -10,4 +11,4 @@ DerivedData/
Package.resolved
.benchmarkBaselines/
.swift-version
.docc-build
.docc-build
29 changes: 29 additions & 0 deletions Examples/open-telemetry/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// swift-tools-version:6.1
import PackageDescription

let package = Package(
name: "open-telemetry",
platforms: [.macOS(.v15)],
products: [
.executable(name: "example", targets: ["Example"])
],
dependencies: [
// TODO: Change to remote once Distributed Tracing support was merged into main and/or tagged
.package(path: "../../"),
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"),
.package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.0.0"),
.package(url: "https://github.com/swift-otel/swift-otel.git", exact: "1.0.0-alpha.1"),
],
targets: [
.executableTarget(
name: "Example",
dependencies: [
.product(name: "Valkey", package: "valkey-swift"),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "Tracing", package: "swift-distributed-tracing"),
.product(name: "OTel", package: "swift-otel"),
]
)
],
swiftLanguageModes: [.v6]
)
128 changes: 128 additions & 0 deletions Examples/open-telemetry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# OpenTelemetry example

An example HTTP server that uses a Valkey client, both of which emit Distributed Tracing spans
via [Swift OTel](https://github.com/swift-otel/swift-otel).

## Overview

This example bootstraps Swift OTel to export Distributed Tracing spans to Jaeger.

It then starts a Hummingbird HTTP server along with its associated middleware for instrumentation.

Finally, the server uses a Valkey client in its request handler to demonstrate the spans
created by executing various Valkey commands.

## Testing

The example uses [Docker Compose](https://docs.docker.com/compose) to run a Valkey server alongside Jaeger to collect
and visualize the spans from the HTTP server and Valkey client, which is running on your local machine.

### Running Valkey and Jaeger

In one terminal window, run the following command:

```console
% docker compose up
[+] Running 4/4
✔ Network open-telemetry_default Created 0.0s
✔ Volume "open-telemetry_valkey_data" Created 0.0s
✔ Container open-telemetry-jaeger-1 Created 0.0s
✔ Container open-telemetry-valkey-1 Created 0.0s
...
```

### Running the server

Now, in another terminal, run the server locally using the following command:

```console
% swift run
```

### Making some requests

Finally, in a third terminal, make a request to the server:

```console
% curl http://localhost:8080/compute/42
```

The example server fakes an expensive algorithm which is hard-coded to take a couple of seconds to complete.
That's why the first request will take a decent amount of time.

Now, make the same request again:

```console
% curl http://localhost:8080/compute/42
```

You should see that it returns instantaniously. We successfully cached the previously computed value in Valkey
and can now read it from the cache instead of re-computing it each time.

### Visualizing the traces using Jaeger UI

Visit Jaeger UI in your browser at [localhost:16686](http://localhost:16686).

Select `example` from the dropdown and click `Find Traces`.

You should see a handful of traces, including:

#### `/compute/{x}` with an execution time of ~ 3.2 seconds

This corresponds to the first request to `/42` where we had to compute the value. Click on this trace to reveal
its spans. The root span represents our entire Hummingbird request handling. Nested inside are three child spans:

1. `HGET`: Shows the `HGET` Valkey command used to look up the cached value for `42`.
2. `compute`: Represents our expensive algorithm. We can see that this takes up the majority of the entire trace.
3. `HSET`: Shows the `HSET` Valkey command sent to store the computed value for future retrieval.

#### `/compute/{x}` with an execution time of a few milliseconds

This span corresponds to a subsequent request to `/42` where we could utelize our cache to avoid the
expensive computation. Click on this trace to reveal its spans. Like before, the root span represents
the Hummingbird request handling. We can also see a single child span:

1. `HGET`: Shows the `HGET` Valkey command used to look up the cached value for `42`.

### Making some more requests

The example also comes with a few more API endpoints to demonstrate other Valkey commands:

#### Pipelined commands

Send the following request to kick off multiple pipelined commands:

```console
% curl http://localhost:8080/multi
```

This will run three pipelined `EVAL` commands and produces a trace made up of the following spans:

1. `/multi`: The Hummingbird request handling.
2. `MULTI`: The Valkey client span representing the execution of the pipelined commands.

Click on the `MULTI` span to reveal its attributes. New here are the following two attributes:

- `db.operation.batch.size`: This is set to `3` and represents the number of pipelined commands.
- `db.operation.name`: This is set to `MULTI EVAL`, showing that the pipeline consists only of `EVAL` commands.

#### Failing commands

Send the following request to send some gibberish to Valkey resulting in an error:

```console
% curl http://localhost:8080/error
```

This will send an `EVAL` command with invalid script contents (`EVAL not a script`) resulting in a trace
made up of the following spans:

1. `/error`: The Hummingbird request handling.
2. `EVAL`: The Valkey client span representing the failed `EVAL` command.

Click on the `EVAL` span to reveal its attributes. New here are the following two attributes:

- `db.response.status_code`: This is set to `ERR` and represents the prefix of the simple error returned
by Valkey.
- `error`: This is set to `true` indicating that the operation failed. In Jaeger, this is additionally displayed
via a red exclamation mark next to the span name.
80 changes: 80 additions & 0 deletions Examples/open-telemetry/Sources/Example/Example.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import Hummingbird
import Logging
import OTel
import ServiceLifecycle
import Tracing
import Valkey

@main
struct Example {
static func main() async throws {
let observability = try bootstrapObservability()
let logger = Logger(label: "example")

let valkeyClient = ValkeyClient(
.hostname("localhost"),
logger: logger
)

let router = Router()
router.add(middleware: TracingMiddleware())
router.add(middleware: LogRequestsMiddleware(.info))

router.get("/compute/:x") { _, context in
let x = try context.parameters.require("x", as: Int.self)

func expensiveAlgorithm(_ x: Int) async throws -> Int {
try await withSpan("compute") { span in
span.attributes["input"] = x
try await Task.sleep(for: .seconds(3))
return x * 2
}
}

if let cachedResult = try await valkeyClient.hget("values", field: "\(x)") {
return cachedResult
}

let result = try await expensiveAlgorithm(x)

try await valkeyClient.hset("values", data: [.init(field: "\(x)", value: "\(result)")])

return ByteBuffer(string: "\(result)")
}

router.get("/multi") { _, _ in
_ = await valkeyClient.execute(
EVAL(script: "return '1'"),
EVAL(script: "return '2'"),
EVAL(script: "return '3'")
)
return HTTPResponse.Status.ok
}

router.get("/error") { _, _ in
_ = try? await valkeyClient.eval(script: "not a script")
return HTTPResponse.Status.ok
}

var app = Application(router: router)
app.addServices(observability, valkeyClient)

try await app.runService()
}

private static func bootstrapObservability() throws -> some Service {
LoggingSystem.bootstrap(
StreamLogHandler.standardOutput(label:metadataProvider:),
metadataProvider: OTel.makeLoggingMetadataProvider()
)

var configuration = OTel.Configuration.default
configuration.serviceName = "example"

// For now, valkey-swift only supports Distributed Tracing so we disable the other signals.
configuration.logs.enabled = false
configuration.metrics.enabled = false

return try OTel.bootstrap(configuration: configuration)
}
}
18 changes: 18 additions & 0 deletions Examples/open-telemetry/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
services:
valkey:
image: valkey/valkey:8.0
ports:
- 6379:6379
healthcheck:
test: ["CMD", "valkey-cli", "--raw", "incr", "ping"]
volumes:
- valkey_data:/data

jaeger:
image: jaegertracing/all-in-one:latest
ports:
- 4318:4318 # OTLP/HTTP receiver
- 16686:16686 # Jaeger Web UI

volumes:
valkey_data:
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ let package = Package(
],
traits: [
.trait(name: "ServiceLifecycleSupport"),
.default(enabledTraits: ["ServiceLifecycleSupport"]),
.trait(name: "DistributedTracingSupport"),
.default(enabledTraits: ["ServiceLifecycleSupport", "DistributedTracingSupport"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.4"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.3"),
.package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.29.0"),
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.23.0"),
Expand All @@ -39,6 +41,7 @@ let package = Package(
.byName(name: "_ValkeyConnectionPool"),
.product(name: "DequeModule", package: "swift-collections"),
.product(name: "Logging", package: "swift-log"),
.product(name: "Tracing", package: "swift-distributed-tracing", condition: .when(traits: ["DistributedTracingSupport"])),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "NIOSSL", package: "swift-nio-ssl"),
Expand Down
14 changes: 14 additions & 0 deletions Sources/Valkey/Commands/BitmapCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ public struct BITCOUNT: ValkeyCommand {
}
public typealias Response = Int

@inlinable public static var name: String { "BITCOUNT" }

public var key: ValkeyKey
public var range: Range?

Expand Down Expand Up @@ -247,6 +249,8 @@ public struct BITFIELD: ValkeyCommand {
}
public typealias Response = RESPToken.Array

@inlinable public static var name: String { "BITFIELD" }

public var key: ValkeyKey
public var operations: [Operation]

Expand Down Expand Up @@ -287,6 +291,8 @@ public struct BITFIELDRO: ValkeyCommand {
}
public typealias Response = [Int]

@inlinable public static var name: String { "BITFIELD_RO" }

public var key: ValkeyKey
public var getBlocks: [GetBlock]

Expand Down Expand Up @@ -328,6 +334,8 @@ public struct BITOP: ValkeyCommand {
}
public typealias Response = Int

@inlinable public static var name: String { "BITOP" }

public var operation: Operation
public var destkey: ValkeyKey
public var keys: [ValkeyKey]
Expand Down Expand Up @@ -405,6 +413,8 @@ public struct BITPOS: ValkeyCommand {
}
public typealias Response = Int

@inlinable public static var name: String { "BITPOS" }

public var key: ValkeyKey
public var bit: Int
public var range: Range?
Expand All @@ -429,6 +439,8 @@ public struct BITPOS: ValkeyCommand {
public struct GETBIT: ValkeyCommand {
public typealias Response = Int

@inlinable public static var name: String { "GETBIT" }

public var key: ValkeyKey
public var offset: Int

Expand All @@ -451,6 +463,8 @@ public struct GETBIT: ValkeyCommand {
public struct SETBIT: ValkeyCommand {
public typealias Response = Int

@inlinable public static var name: String { "SETBIT" }

public var key: ValkeyKey
public var offset: Int
public var value: Int
Expand Down
Loading
Loading