Skip to content

Commit 3b5ed17

Browse files
committed
Add TraceState and documentation 📖
1 parent a49eced commit 3b5ed17

File tree

7 files changed

+368
-49
lines changed

7 files changed

+368
-49
lines changed

README.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
[![Swift 5.2](https://img.shields.io/badge/Swift-5.2-ED523F.svg?style=flat)](https://swift.org/download/)
44
[![CI](https://github.com/slashmo/swift-w3c-trace-context/workflows/CI/badge.svg)](https://github.com/slashmo/swift-w3c-trace-context/actions?query=workflow%3ACI)
55

6-
This Swift package provides a struct `W3C.TraceContext` conforming to the [W3C Trace Context standard (Level 1)](https://www.w3.org/TR/trace-context).
6+
This Swift package provides a struct `TraceContext` conforming to
7+
the [W3C Trace Context standard (Level 1)](https://www.w3.org/TR/2020/REC-trace-context-1-20200206/).
78

89
## Installation
910

@@ -18,3 +19,38 @@ Then, add the `W3CTraceContext` library as a product dependency to each target y
1819
```swift
1920
.product(name: "W3CTraceContext", package: "swift-w3c-trace-context")
2021
```
22+
23+
## Usage
24+
25+
### 1️⃣ Extract raw HTTP header values for `traceparent` and `tracestate`
26+
27+
```swift
28+
let traceParentValue = headers.first(name: TraceParent.headerName)!
29+
let traceStateValue = headers.first(name: TraceState.headerName)!
30+
```
31+
32+
### 2️⃣ Attempt to parse both values, creating a `TraceContext`
33+
34+
```swift
35+
guard let traceContext = TraceContext(parent: traceParentValue, state: traceStateValue) else {
36+
// received invalid HTTP header values
37+
return
38+
}
39+
```
40+
41+
### 3️⃣ Inject `traceparent` and `tracestate` into subsequent request headers
42+
43+
```swift
44+
headers.replaceOrAdd(name: TraceParent.headerName, value: traceContext.parent.rawValue)
45+
headers.replaceOrAdd(name: TraceState.headerName, value: traceContext.state.rawValue)
46+
```
47+
48+
## Contributing
49+
50+
Please make sure to run the `./scripts/sanity.sh` script when contributing, it checks formatting and similar things.
51+
52+
You can ensure it always runs and passes before you push by installing a pre-push hook with git:
53+
54+
```sh
55+
echo './scripts/sanity.sh' > .git/hooks/pre-push
56+
```

Sources/W3CTraceContext/TraceContext.swift

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,31 @@
1111
//
1212
//===----------------------------------------------------------------------===//
1313

14-
public enum W3C {}
14+
/// An implementation of [W3C TraceContext](https://www.w3.org/TR/2020/REC-trace-context-1-20200206/),
15+
/// combining `TraceParent` and `TraceState`.
16+
public struct TraceContext: Equatable {
17+
/// The `TraceParent` identifying this trace context.
18+
public let parent: TraceParent
1519

16-
extension W3C {
17-
public struct TraceContext: Equatable {
18-
public let parent: TraceParent
20+
/// The `TraceState` containing potentially vendor-specific trace information.
21+
public let state: TraceState
1922

20-
public init(parent: TraceParent) {
21-
self.parent = parent
22-
}
23+
init(parent: TraceParent, state: TraceState) {
24+
self.parent = parent
25+
self.state = state
26+
}
2327

24-
public init?(parent parentRawValue: String) {
25-
guard let parent = TraceParent(rawValue: parentRawValue) else { return nil }
26-
self.parent = parent
27-
}
28+
/// Create a `TraceContext` by parsing the given header values.
29+
///
30+
/// - Parameters:
31+
/// - parentRawValue: HTTP header value for the `traceparent` key.
32+
/// - stateRawValue: HTTP header value for the `tracestate` key.
33+
///
34+
/// When receiving multiple header fields for `tracestate`, the `state` argument should be a joined, comma-separated, `String`
35+
/// of all values according to [HTTP RFC 7230: Field Order](https://httpwg.org/specs/rfc7230.html#rfc.section.3.2.2).
36+
public init?(parent parentRawValue: String, state stateRawValue: String) {
37+
guard let parent = TraceParent(rawValue: parentRawValue) else { return nil }
38+
self.parent = parent
39+
self.state = TraceState(rawValue: stateRawValue) ?? TraceState([])
2840
}
2941
}

Sources/W3CTraceContext/TraceParent.swift

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,39 +11,62 @@
1111
//
1212
//===----------------------------------------------------------------------===//
1313

14-
extension W3C {
15-
public struct TraceParent {
16-
public let traceID: String
17-
public let parentID: String
18-
public let traceFlags: String
14+
/// Represents the incoming request in a tracing system in a common format, understood by all vendors.
15+
///
16+
/// Example raw value: `00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01`
17+
///
18+
/// - SeeAlso: [W3C TraceContext: TraceParent](https://www.w3.org/TR/2020/REC-trace-context-1-20200206/#traceparent-header)
19+
public struct TraceParent {
20+
/// The ID of the whole trace forest, used to uniquely identify a distributed trace through a system.
21+
///
22+
/// - SeeAlso: [W3C TraceContext: trace-id](https://www.w3.org/TR/2020/REC-trace-context-1-20200206/#trace-id)
23+
public let traceID: String
1924

20-
public init(traceID: String, parentID: String, traceFlags: String) {
21-
self.traceID = traceID
22-
self.parentID = parentID
23-
self.traceFlags = traceFlags
24-
}
25+
/// The ID of the incoming request as known by the caller (in some tracing systems, this is known as the span-id, where a span is the execution of
26+
/// a client request).
27+
///
28+
/// - SeeAlso: [W3C TraceContext: parent-id](https://www.w3.org/TR/2020/REC-trace-context-1-20200206/#parent-id)
29+
public let parentID: String
2530

26-
public var sampled: Bool {
27-
self.traceFlags == "01"
28-
}
31+
/// An 8-bit field that controls tracing flags such as sampling, trace level, etc.
32+
///
33+
/// - SeeAlso: [W3C TraceContext: trace-flags](https://www.w3.org/TR/2020/REC-trace-context-1-20200206/#trace-flags)
34+
public let traceFlags: String
2935

30-
public static let headerName = "traceparent"
36+
init(traceID: String, parentID: String, traceFlags: String) {
37+
self.traceID = traceID
38+
self.parentID = parentID
39+
self.traceFlags = traceFlags
40+
}
3141

32-
private static let version = "00"
42+
/// When `true`, the least significant bit (right-most), denotes that the caller may have recorded trace data.
43+
/// When `false`, the caller did not record trace data out-of-band.
44+
///
45+
/// - SeeAlso: [W3C TraceContext: Sampled flag](https://www.w3.org/TR/2020/REC-trace-context-1-20200206/#sampled-flag)
46+
public var sampled: Bool {
47+
self.traceFlags == "01"
3348
}
3449

35-
// TODO: Trace State
50+
/// The HTTP header name for `TraceParent`.
51+
public static let headerName = "traceparent"
52+
53+
/// Hard-coded version to "00" as it's the only version currently supported by this package.
54+
private static let version = "00"
3655
}
3756

38-
extension W3C.TraceParent: Equatable {
57+
extension TraceParent: Equatable {
58+
// custom implementation to avoid automatic equality check of rawValue which is unnecessary computational overhead
3959
public static func == (lhs: Self, rhs: Self) -> Bool {
4060
lhs.traceID == rhs.traceID
4161
&& lhs.parentID == rhs.parentID
4262
&& lhs.traceFlags == rhs.traceFlags
4363
}
4464
}
4565

46-
extension W3C.TraceParent: RawRepresentable {
66+
extension TraceParent: RawRepresentable {
67+
/// Initialize a `TraceParent` from an HTTP header value. Fails if the value cannot be parsed.
68+
///
69+
/// - Parameter rawValue: The value of the traceparent HTTP header.
4770
public init?(rawValue: String) {
4871
guard rawValue.count == 55 else { return nil }
4972

@@ -72,7 +95,14 @@ extension W3C.TraceParent: RawRepresentable {
7295
self.traceFlags = String(traceFlagsComponent)
7396
}
7497

98+
/// A `String` representation of this trace parent, suitable for injecting into HTTP headers.
7599
public var rawValue: String {
76100
"\(Self.version)-\(self.traceID)-\(self.parentID)-\(self.traceFlags)"
77101
}
78102
}
103+
104+
extension TraceParent: CustomStringConvertible {
105+
public var description: String {
106+
self.rawValue
107+
}
108+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift W3C Trace Context open source project
4+
//
5+
// Copyright (c) 2020 Moritz Lang and the Swift W3C Trace Context project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
//
10+
// SPDX-License-Identifier: Apache-2.0
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
/// Provides additional vendor-specific trace identification information across different distributed tracing systems and is a companion header for the
15+
/// traceparent field.
16+
///
17+
/// Example raw value: `rojo=00f067aa0ba902b7,congo=t61rcWkgMzE`
18+
///
19+
/// - SeeAlso: [W3C TraceContext: TraceState](https://www.w3.org/TR/2020/REC-trace-context-1-20200206/#tracestate-header)
20+
public struct TraceState {
21+
typealias Storage = [(vendor: String, value: String)]
22+
23+
private var _storage = Storage()
24+
25+
init(_ storage: Storage) {
26+
self._storage = storage
27+
}
28+
29+
/// The HTTP header name for `TraceState`.
30+
public static let headerName = "tracestate"
31+
}
32+
33+
extension TraceState: Equatable {
34+
public static func == (lhs: Self, rhs: Self) -> Bool {
35+
guard lhs._storage.count == rhs._storage.count else { return false }
36+
for (index, (lhsVendor, lhsValue)) in lhs._storage.enumerated() {
37+
let (rhsVendor, rhsValue) = rhs._storage[index]
38+
guard lhsVendor == rhsVendor, lhsValue == rhsValue else { return false }
39+
}
40+
return true
41+
}
42+
}
43+
44+
extension TraceState: CustomStringConvertible {
45+
public var description: String {
46+
rawValue
47+
}
48+
}
49+
50+
extension TraceState: RawRepresentable {
51+
/// Initialize a `TraceState` from an HTTP header value. Fails if the value cannot be parsed.
52+
///
53+
/// - Parameter rawValue: The value of the tracestate HTTP header.
54+
/// - Note: When receiving multiple header fields for `tracestate`, `rawValue` should be a joined, comma-separated `String` of all values
55+
/// according to [HTTP RFC 7230: Field Order](https://httpwg.org/specs/rfc7230.html#rfc.section.3.2.2).
56+
public init?(rawValue: String) {
57+
let keyValuePairs = rawValue.split(separator: ",")
58+
let horizontalSpaces = Set<Character>([" ", "\t"])
59+
var storage = Storage()
60+
61+
for var rest in keyValuePairs {
62+
while !rest.isEmpty {
63+
if horizontalSpaces.contains(rest[rest.startIndex]) {
64+
rest.removeFirst()
65+
} else if horizontalSpaces.contains(rest[rest.index(before: rest.endIndex)]) {
66+
rest.removeLast()
67+
} else {
68+
print(rest)
69+
break
70+
}
71+
}
72+
73+
guard !rest.isEmpty else { return }
74+
75+
var vendor = ""
76+
77+
while vendor.count < 256, !rest.hasPrefix("="), !rest.isEmpty {
78+
let next = rest.removeFirst()
79+
switch next {
80+
case "a" ... "z", "0" ... "9", "_", "-", "*", "/":
81+
vendor.append(next)
82+
case "@":
83+
guard !rest.hasPrefix("=") else { return nil }
84+
vendor.append(next)
85+
default:
86+
return nil
87+
}
88+
}
89+
90+
guard !vendor.isEmpty, rest.hasPrefix("=") else { return nil }
91+
rest.removeFirst()
92+
93+
var value = ""
94+
95+
while value.count < 256, !rest.isEmpty {
96+
value += String(rest.removeFirst())
97+
}
98+
99+
storage.append((vendor: vendor, value: value))
100+
}
101+
102+
self._storage = storage
103+
}
104+
105+
/// A `String` representation of this trace state, suitable for injecting into HTTP headers.
106+
public var rawValue: String {
107+
self._storage.map { "\($0)=\($1)" }.joined(separator: ",")
108+
}
109+
}

Tests/W3CTraceContextTests/TraceContextTests.swift

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,54 @@
1111
//
1212
//===----------------------------------------------------------------------===//
1313

14-
import W3CTraceContext
14+
@testable import W3CTraceContext
1515
import XCTest
1616

1717
final class TraceContextTests: XCTestCase {
1818
func testInitWithValidRawValues() {
1919
let traceParentRawValue = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
20+
let traceStateRawValue = "rojo=00f067aa0ba902b7"
2021

21-
guard let traceContext = W3C.TraceContext(parent: traceParentRawValue) else {
22+
guard let traceContext = TraceContext(parent: traceParentRawValue, state: traceStateRawValue) else {
2223
XCTFail("Could not decode valid trace context")
2324
return
2425
}
2526

2627
XCTAssertEqual(
2728
traceContext,
28-
W3C.TraceContext(
29-
parent: W3C.TraceParent(
29+
TraceContext(
30+
parent: TraceParent(
3031
traceID: "0af7651916cd43dd8448eb211c80319c",
3132
parentID: "b7ad6b7169203331",
3233
traceFlags: "01"
33-
)
34+
),
35+
state: TraceState([("rojo", "00f067aa0ba902b7")])
3436
)
3537
)
3638
}
3739

3840
func testInitWithInvalidTraceParent() {
39-
XCTAssertNil(W3C.TraceContext(parent: "invalid"))
41+
XCTAssertNil(TraceContext(parent: "invalid", state: ""))
42+
}
43+
44+
func testInitWithValidTraceParentAndInvalidTraceState() {
45+
let traceParentRawValue = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
46+
47+
guard let traceContext = TraceContext(parent: traceParentRawValue, state: "invalid") else {
48+
XCTFail("Could not decode valid trace context")
49+
return
50+
}
51+
52+
XCTAssertEqual(
53+
traceContext,
54+
TraceContext(
55+
parent: TraceParent(
56+
traceID: "0af7651916cd43dd8448eb211c80319c",
57+
parentID: "b7ad6b7169203331",
58+
traceFlags: "01"
59+
),
60+
state: TraceState([])
61+
)
62+
)
4063
}
4164
}

0 commit comments

Comments
 (0)