Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
78 changes: 75 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ let package = Package(
name: "SignozSwift",
targets: ["SignozSwift"]
),
.library(
name: "SignozVapor",
targets: ["SignozVapor"]
),
],
dependencies: [
.package(
Expand Down Expand Up @@ -70,6 +74,10 @@ let package = Package(
url: "https://github.com/onevcat/Rainbow.git",
from: "4.0.0"
),
.package(
url: "https://github.com/vapor/vapor.git",
from: "4.89.0"
),
Comment thread
underthestars-zhy marked this conversation as resolved.
],
targets: [
.target(
Expand Down Expand Up @@ -99,6 +107,13 @@ let package = Package(
),
]
),
.target(
name: "SignozVapor",
dependencies: [
"SignozSwift",
.product(name: "Vapor", package: "vapor"),
]
),
.testTarget(
name: "SignozSwiftTests",
dependencies: [
Expand Down
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,19 @@ info("Request handled", attributes: ["status": 200])

### Vapor HTTP Backend

For Vapor projects, depend on the `SignozVapor` product instead of `SignozSwift` — it re-exports everything plus the tracing middleware:

```swift
import SignozSwift
import Vapor
.target(
name: "MyVaporApp",
dependencies: [
.product(name: "SignozVapor", package: "SignozSwift"),
]
),
```

```swift
import SignozVapor

func configure(_ app: Application) throws {
Signoz.start(serviceName: "my-vapor-api") {
Expand All @@ -58,22 +68,24 @@ func configure(_ app: Application) throws {
$0.headers = ["signoz-ingestion-key": "..."]
$0.environment = "production"
$0.serviceVersion = "1.0.0"
// Vapor's HTTP metrics (http_requests_total, http_request_duration_seconds)
// are automatically exported via the swift-metrics bridge. Zero extra code.
}

// Automatic request tracing — creates a .server span for every HTTP request
// with OTel semantic convention attributes and W3C trace context propagation
app.middleware.use(SignozTracingMiddleware())

app.get("users") { req async throws -> [User] in
try await span("GET /users", kind: .server) { s in
s.setAttribute(key: "http.method", value: "GET")
return try await db.fetchUsers()
}
// Spans created here are automatically nested under the request span
try await db.fetchUsers()
}
}

// In entrypoint:
defer { Signoz.shutdown() }
```

`SignozTracingMiddleware` sets `http.method`, `http.target`, `http.scheme`, `http.status_code`, and `http.route` on each span. Span names use the matched route pattern (e.g. `GET /users/:id`) when available. Vapor's HTTP metrics (`http_requests_total`, `http_request_duration_seconds`) are also automatically exported via the swift-metrics bridge.

### ArgumentParser CLI

```swift
Expand Down Expand Up @@ -293,7 +305,7 @@ let attrs: [String: AttributeValue] = [
SignozSwift wraps the official OpenTelemetry Swift SDK — it does not reinvent any OTel types.

- **[opentelemetry-swift-core](https://github.com/open-telemetry/opentelemetry-swift-core) 2.3.0** — `OpenTelemetryApi`, `OpenTelemetrySdk`
- **[opentelemetry-swift](https://github.com/open-telemetry/opentelemetry-swift) 3.0.0** — OTLP proto adapters, URLSession instrumentation, ResourceExtension, SignPost integration, SwiftMetricsShim
- **[opentelemetry-swift](https://github.com/photon-hq/opentelemetry-swift) 3.0.0** — OTLP proto adapters, URLSession instrumentation, ResourceExtension, SignPost integration, SwiftMetricsShim
- **[grpc-swift-2](https://github.com/grpc/grpc-swift-2) 2.2.1** — gRPC transport (v2, async/await)
- **[grpc-swift-extras](https://github.com/grpc/grpc-swift-extras) 2.1.1** — OTel tracing interceptors for automatic gRPC span injection
- **[Rainbow](https://github.com/onevcat/Rainbow) 4.x** — Colored console output
Expand Down
59 changes: 8 additions & 51 deletions Sources/SignozSwift/Tracing/OTelTracingBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ import OpenTelemetrySdk
struct OTelTracingBridge: Tracing.Tracer {
typealias Span = BridgedSpan

private static let traceparentKey = "traceparent"
private static let tracestateKey = "tracestate"

func startSpan<Instant: TracerInstant>(
_ operationName: String,
context: @autoclosure () -> ServiceContext,
Expand Down Expand Up @@ -64,17 +61,10 @@ struct OTelTracingBridge: Tracing.Tracer {
return
}

// W3C traceparent: version-traceId-spanId-traceFlags
let flags = spanContext.traceFlags.sampled ? "01" : "00"
let traceparent = "00-\(spanContext.traceId.hexString)-\(spanContext.spanId.hexString)-\(flags)"
injector.inject(traceparent, forKey: Self.traceparentKey, into: &carrier)

// W3C tracestate (if non-empty)
if !spanContext.traceState.entries.isEmpty {
let tracestate = spanContext.traceState.entries
.map { "\($0.key)=\($0.value)" }
.joined(separator: ",")
injector.inject(tracestate, forKey: Self.tracestateKey, into: &carrier)
let (traceparent, tracestate) = W3CTraceContext.serialize(spanContext)
injector.inject(traceparent, forKey: W3CTraceContext.traceparentKey, into: &carrier)
if let tracestate {
injector.inject(tracestate, forKey: W3CTraceContext.tracestateKey, into: &carrier)
}
}

Expand All @@ -83,46 +73,13 @@ struct OTelTracingBridge: Tracing.Tracer {
into context: inout ServiceContext,
using extractor: Extract
) where Extract.Carrier == Carrier {
guard let traceparent = extractor.extract(key: Self.traceparentKey, from: carrier) else {
return
}

// Parse W3C traceparent: version-traceId-spanId-traceFlags
let parts = traceparent.split(separator: "-")
guard parts.count == 4, parts[0] == "00" else {
guard let traceparent = extractor.extract(key: W3CTraceContext.traceparentKey, from: carrier) else {
return
}

let traceId = TraceId(fromHexString: String(parts[1]))
let spanId = SpanId(fromHexString: String(parts[2]))
guard traceId.isValid, spanId.isValid else {
return
let tracestate = extractor.extract(key: W3CTraceContext.tracestateKey, from: carrier)
if let spanContext = W3CTraceContext.parse(traceparent: traceparent, tracestate: tracestate) {
context.otelSpanContext = spanContext
}

let sampled = UInt8(String(parts[3]), radix: 16).map { $0 & 0x01 != 0 } ?? false
var traceFlags = TraceFlags()
traceFlags.setIsSampled(sampled)

// Parse tracestate if present
var traceState = TraceState()
if let tracestateHeader = extractor.extract(key: Self.tracestateKey, from: carrier) {
for entry in tracestateHeader.split(separator: ",") {
let kv = entry.split(separator: "=", maxSplits: 1)
if kv.count == 2 {
traceState = traceState.setting(
key: String(kv[0]).trimmingCharacters(in: .whitespaces),
value: String(kv[1]).trimmingCharacters(in: .whitespaces)
)
}
}
}

let spanContext = SpanContext.createFromRemoteParent(
traceId: traceId,
spanId: spanId,
traceFlags: traceFlags,
traceState: traceState
)
context.otelSpanContext = spanContext
}
}
67 changes: 67 additions & 0 deletions Sources/SignozSwift/Tracing/W3CTraceContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
@preconcurrency import OpenTelemetryApi

/// Shared W3C Trace Context parsing and serialization.
///
/// Used by both `OTelTracingBridge` (for gRPC distributed tracing) and
/// `SignozTracingMiddleware` (for Vapor HTTP tracing).
public enum W3CTraceContext {

public static let traceparentKey = "traceparent"
public static let tracestateKey = "tracestate"

/// Parse W3C `traceparent` and optional `tracestate` header values into a `SpanContext`.
public static func parse(traceparent: String, tracestate: String? = nil) -> SpanContext? {
// W3C spec: reject only version "ff"; accept unknown future versions
// for forward compatibility (the first 55 chars are guaranteed stable).
// Future versions may append extra `-` delimited fields, so require >= 4 parts.
let parts = traceparent.split(separator: "-")
guard parts.count >= 4, parts[0] != "ff" else {
return nil
}
Comment on lines +17 to +20
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The W3C version check was changed from parts[0] == "00" (strict version-00-only) to parts[0] != "ff" (forward-compatible, per W3C spec). This is a behavioral change that's called out in the PR description, but no tests cover it. There should be tests verifying that: (1) a future version like "01" is accepted, and (2) version "ff" is rejected. The existing test suite covers W3C trace context propagation at line 771 but only tests with version "00".

Copilot uses AI. Check for mistakes.

let traceId = TraceId(fromHexString: String(parts[1]))
let spanId = SpanId(fromHexString: String(parts[2]))
guard traceId.isValid, spanId.isValid else {
return nil
}

let sampled = UInt8(String(parts[3]), radix: 16).map { $0 & 0x01 != 0 } ?? false
var traceFlags = TraceFlags()
traceFlags.setIsSampled(sampled)

var state = TraceState()
if let tracestateHeader = tracestate {
for entry in tracestateHeader.split(separator: ",") {
let kv = entry.split(separator: "=", maxSplits: 1)
if kv.count == 2 {
state = state.setting(
key: String(kv[0]).trimmingCharacters(in: .whitespaces),
value: String(kv[1]).trimmingCharacters(in: .whitespaces)
)
}
}
}

return SpanContext.createFromRemoteParent(
traceId: traceId,
spanId: spanId,
traceFlags: traceFlags,
traceState: state
)
}

/// Serialize a `SpanContext` into W3C `traceparent` and optional `tracestate` header values.
public static func serialize(_ spanContext: SpanContext) -> (traceparent: String, tracestate: String?) {
let flags = spanContext.traceFlags.sampled ? "01" : "00"
let traceparent = "00-\(spanContext.traceId.hexString)-\(spanContext.spanId.hexString)-\(flags)"

var tracestate: String? = nil
if !spanContext.traceState.entries.isEmpty {
tracestate = spanContext.traceState.entries
.map { "\($0.key)=\($0.value)" }
.joined(separator: ",")
}

return (traceparent, tracestate)
}
}
Loading
Loading