Skip to content

Commit 8da92e7

Browse files
author
Ignacio Bonafonte
authored
Merge pull request #158 from nachoBonafonte/Implement-a-Jaeger-propagator
Implement a Jaeger propagator for context and baggage
2 parents b43a137 + fab745b commit 8da92e7

File tree

9 files changed

+542
-55
lines changed

9 files changed

+542
-55
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright 2020, OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
import Foundation
17+
18+
/**
19+
* Implementation of the Jaeger propagation protocol. See
20+
* https://www.jaegertracing.io/docs/client-libraries/#propagation-format
21+
*/
22+
23+
public class JaegerBaggagePropagator: TextMapBaggagePropagator {
24+
public static let baggageHeader = "jaeger-baggage"
25+
public static let baggagePrefix = "uberctx-"
26+
27+
public var fields: Set<String> = [baggageHeader]
28+
29+
public init() {}
30+
31+
public func inject<S>(baggage: Baggage, carrier: inout [String: String], setter: S) where S: Setter {
32+
baggage.getEntries().forEach {
33+
setter.set(carrier: &carrier, key: JaegerBaggagePropagator.baggagePrefix + $0.key.name, value: $0.value.string)
34+
}
35+
}
36+
37+
public func extract<G>(carrier: [String: String], getter: G) -> Baggage? where G: Getter {
38+
let builder = OpenTelemetry.instance.baggageManager.baggageBuilder()
39+
40+
carrier.forEach {
41+
if $0.key.hasPrefix(JaegerBaggagePropagator.baggagePrefix) {
42+
if $0.key.count == JaegerBaggagePropagator.baggagePrefix.count {
43+
return
44+
}
45+
46+
if let key = EntryKey(name: String($0.key.dropFirst(JaegerBaggagePropagator.baggagePrefix.count))),
47+
let value = EntryValue(string: $0.value)
48+
{
49+
builder.put(key: key, value: value, metadata: nil)
50+
}
51+
} else if $0.key == JaegerBaggagePropagator.baggageHeader {
52+
$0.value.split(separator: ",").forEach { entry in
53+
let keyValue = entry.split(separator: "=")
54+
if keyValue.count != 2 {
55+
return
56+
}
57+
if let key = EntryKey(name: String(keyValue[0])),
58+
let value = EntryValue(string: String(keyValue[1]))
59+
{
60+
builder.put(key: key, value: value, metadata: nil)
61+
}
62+
}
63+
}
64+
}
65+
66+
return builder.build()
67+
}
68+
}

Sources/OpenTelemetryApi/Trace/Propagation/B3Propagator.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ import Foundation
2020
* https://github.com/openzipkin/b3-propagation
2121
*/
2222
public class B3Propagator: TextMapPropagator {
23-
public static let traceIdHeader = "X-B3-TraceId"
24-
public static let spanIdHeader = "X-B3-SpanId"
25-
public static let sampledHeader = "X-B3-Sampled"
26-
public static let trueInt = "1"
27-
public static let falseInt = "0"
28-
public static let combinedHeader = "b3"
29-
public static let combinedHeaderDelimiter = "-"
23+
static let traceIdHeader = "X-B3-TraceId"
24+
static let spanIdHeader = "X-B3-SpanId"
25+
static let sampledHeader = "X-B3-Sampled"
26+
static let trueInt = "1"
27+
static let falseInt = "0"
28+
static let combinedHeader = "b3"
29+
static let combinedHeaderDelimiter = "-"
3030

3131
public let fields: Set<String> = [traceIdHeader, spanIdHeader, sampledHeader]
3232

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2020, OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
import Foundation
17+
18+
/**
19+
* Implementation of the Jaeger propagation protocol. See
20+
* https://www.jaegertracing.io/docs/client-libraries/#propagation-format
21+
*/
22+
23+
public class JaegerPropagator: TextMapPropagator {
24+
static let propagationHeader = "uber-trace-id"
25+
// Parent span has been deprecated but Jaeger propagation protocol requires it
26+
static let deprecatedParentSpan = "0"
27+
static let propagationHeaderDelimiter: Character = ":"
28+
29+
private static let maxTraceIdLength = 2 * TraceId.size
30+
private static let maxSpanIdLength = 2 * SpanId.size
31+
private static let maxFlagsLength = 2
32+
33+
private static let isSampledChar = "1"
34+
private static let notSampledChar = "0"
35+
36+
private static let sampledFlags = TraceFlags().settingIsSampled(true)
37+
private static let notSampledFlags = TraceFlags().settingIsSampled(false)
38+
39+
public var fields: Set<String> = [propagationHeader]
40+
41+
public init() {}
42+
43+
public func inject<S>(spanContext: SpanContext, carrier: inout [String: String], setter: S) where S: Setter {
44+
guard spanContext.traceId.isValid, spanContext.spanId.isValid else {
45+
return
46+
}
47+
var propagation = ""
48+
propagation += spanContext.traceId.hexString
49+
propagation += String(JaegerPropagator.propagationHeaderDelimiter)
50+
propagation += spanContext.spanId.hexString
51+
propagation += String(JaegerPropagator.propagationHeaderDelimiter)
52+
propagation += JaegerPropagator.deprecatedParentSpan
53+
propagation += String(JaegerPropagator.propagationHeaderDelimiter)
54+
propagation += spanContext.isSampled ? JaegerPropagator.isSampledChar : JaegerPropagator.notSampledChar
55+
setter.set(carrier: &carrier, key: JaegerPropagator.propagationHeader, value: propagation)
56+
}
57+
58+
public func extract<G>(carrier: [String: String], getter: G) -> SpanContext? where G: Getter {
59+
guard let headerValue = getter.get(carrier: carrier, key: JaegerPropagator.propagationHeader), headerValue.count >= 1 else {
60+
return nil
61+
}
62+
63+
var header = headerValue[0]
64+
if header.lastIndex(of: JaegerPropagator.propagationHeaderDelimiter) == nil {
65+
guard let decodedHeader = header.removingPercentEncoding,
66+
let _ = decodedHeader.lastIndex(of: JaegerPropagator.propagationHeaderDelimiter)
67+
else {
68+
return nil
69+
}
70+
header = decodedHeader
71+
}
72+
73+
let parts = header.split(separator: JaegerPropagator.propagationHeaderDelimiter)
74+
guard parts.count == 4 else {
75+
return nil
76+
}
77+
78+
let traceId = String(parts[0])
79+
if !isTraceIdValid(traceId) {
80+
return nil
81+
}
82+
83+
let spanId = String(parts[1])
84+
if !isSpanIdValid(spanId) {
85+
return nil
86+
}
87+
88+
let flags = String(parts[3])
89+
if !isFlagValid(flags) {
90+
return nil
91+
}
92+
93+
return buildSpanContext(traceId: traceId, spanId: spanId, flags: flags)
94+
}
95+
96+
private func buildSpanContext(traceId: String, spanId: String, flags: String) -> SpanContext? {
97+
let flagsInt = Int(flags) ?? 0
98+
let traceFlags = ((flagsInt & 1) == 1) ? JaegerPropagator.sampledFlags : JaegerPropagator.notSampledFlags
99+
let context = SpanContext.createFromRemoteParent(traceId: TraceId(fromHexString: traceId),
100+
spanId: SpanId(fromHexString: spanId),
101+
traceFlags: traceFlags,
102+
traceState: TraceState())
103+
return context.isValid ? context : nil
104+
}
105+
106+
private func isTraceIdValid(_ traceId: String) -> Bool {
107+
return !(traceId.isEmpty || traceId.count > JaegerPropagator.maxTraceIdLength)
108+
}
109+
110+
private func isSpanIdValid(_ spanId: String) -> Bool {
111+
return !(spanId.isEmpty || spanId.count > JaegerPropagator.maxSpanIdLength)
112+
}
113+
114+
private func isFlagValid(_ flags: String) -> Bool {
115+
return !(flags.isEmpty || flags.count > JaegerPropagator.maxFlagsLength)
116+
}
117+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright 2020, OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
@testable import OpenTelemetryApi
17+
import XCTest
18+
19+
class JaegerBaggagePropagatorTests: XCTestCase {
20+
let builder = DefaultBaggageBuilder()
21+
let jaegerPropagator = JaegerBaggagePropagator()
22+
let setter = TestSetter()
23+
let getter = TestGetter()
24+
25+
func testInjectBaggage() {
26+
// Metadata won't be propagated, but it MUST NOT cause ay problem.
27+
let baggage = builder.put(key: "nometa", value: "nometa-value")
28+
.put(key: "nometa", value: "nometa-value")
29+
.put(key: "meta", value: "meta-value", metadata: "somemetadata; someother=foo")
30+
.build()
31+
32+
var carrier = [String: String]()
33+
jaegerPropagator.inject(baggage: baggage, carrier: &carrier, setter: setter)
34+
35+
let expected1 = [JaegerBaggagePropagator.baggagePrefix + "nometa": "nometa-value",
36+
JaegerBaggagePropagator.baggagePrefix + "meta": "meta-value"]
37+
let expected2 = [JaegerBaggagePropagator.baggagePrefix + "meta": "meta-value",
38+
JaegerBaggagePropagator.baggagePrefix + "nometa": "nometa-value"]
39+
XCTAssert(carrier == expected1 || carrier == expected2)
40+
}
41+
42+
func testExtractBaggageWithPrefix() {
43+
var carrier = [String: String]()
44+
carrier[JaegerBaggagePropagator.baggagePrefix + "nometa"] = "nometa-value"
45+
carrier[JaegerBaggagePropagator.baggagePrefix + "meta"] = "meta-value"
46+
carrier["another"] = "value"
47+
48+
let expectedBaggage = builder.put(key: "nometa", value: "nometa-value")
49+
.put(key: "meta", value: "meta-value")
50+
.build()
51+
52+
let result = jaegerPropagator.extract(carrier: carrier, getter: getter)
53+
XCTAssertEqual(result?.getEntries().sorted(), expectedBaggage.getEntries().sorted())
54+
}
55+
56+
func testExtractBaggageWithPrefixEmptyKey() {
57+
var carrier = [String: String]()
58+
carrier[JaegerBaggagePropagator.baggagePrefix] = "value"
59+
60+
let result = jaegerPropagator.extract(carrier: carrier, getter: getter)!
61+
XCTAssertTrue(result.getEntries().isEmpty)
62+
}
63+
64+
func testExtractBaggageWithHeader() {
65+
var carrier = [String: String]()
66+
carrier[JaegerBaggagePropagator.baggageHeader] = "nometa=nometa-value,meta=meta-value"
67+
68+
let expectedBaggage = builder.put(key: "nometa", value: "nometa-value")
69+
.put(key: "meta", value: "meta-value")
70+
.build()
71+
72+
let result = jaegerPropagator.extract(carrier: carrier, getter: getter)
73+
XCTAssertEqual(result?.getEntries().sorted(), expectedBaggage.getEntries().sorted())
74+
}
75+
76+
func testExtractBaggageWithHeaderAndSpaces() {
77+
var carrier = [String: String]()
78+
carrier[JaegerBaggagePropagator.baggageHeader] = "nometa = nometa-value , meta = meta-value"
79+
80+
let expectedBaggage = builder.put(key: "nometa", value: "nometa-value")
81+
.put(key: "meta", value: "meta-value")
82+
.build()
83+
84+
let result = jaegerPropagator.extract(carrier: carrier, getter: getter)
85+
XCTAssertEqual(result?.getEntries().sorted(), expectedBaggage.getEntries().sorted())
86+
}
87+
88+
func testExtractBaggageWithHeaderInvalid() {
89+
var carrier = [String: String]()
90+
carrier[JaegerBaggagePropagator.baggageHeader] = "nometa+novalue"
91+
92+
let result = jaegerPropagator.extract(carrier: carrier, getter: getter)
93+
XCTAssertTrue(result?.getEntries().isEmpty ?? false)
94+
}
95+
96+
func testExtractBaggageWithHeaderAndPrefix() {
97+
var carrier = [String: String]()
98+
carrier[JaegerBaggagePropagator.baggageHeader] = "nometa=nometa-value,meta=meta-value"
99+
carrier[JaegerBaggagePropagator.baggagePrefix + "foo"] = "bar"
100+
101+
let expectedBaggage = builder.put(key: "nometa", value: "nometa-value")
102+
.put(key: "meta", value: "meta-value")
103+
.put(key: "foo", value: "bar")
104+
.build()
105+
106+
let result = jaegerPropagator.extract(carrier: carrier, getter: getter)
107+
XCTAssertEqual(result?.getEntries().sorted(), expectedBaggage.getEntries().sorted())
108+
}
109+
}

Tests/OpenTelemetryApiTests/Baggage/Propagation/W3CBaggagePropagatorTest.swift

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,6 @@
1717
import XCTest
1818

1919
class W3BaggagePropagatorTest: XCTestCase {
20-
struct TestSetter: Setter {
21-
func set(carrier: inout [String: String], key: String, value: String) {
22-
carrier[key] = value
23-
}
24-
}
25-
26-
struct TestGetter: Getter {
27-
func get(carrier: [String: String], key: String) -> [String]? {
28-
if let value = carrier[key] {
29-
return [value]
30-
}
31-
return nil
32-
}
33-
}
34-
3520
let setter = TestSetter()
3621
let getter = TestGetter()
3722
let propagator = W3CBaggagePropagator()
@@ -81,7 +66,7 @@ class W3BaggagePropagatorTest: XCTestCase {
8166

8267
func testExtractFullComplexities() {
8368
let result = propagator.extract(carrier: ["baggage": "key1= value1; metadata-key = value; othermetadata, " +
84-
"key2 =value2 , key3 =\tvalue3 ; "], getter: getter)!
69+
"key2 =value2 , key3 =\tvalue3 ; "], getter: getter)!
8570
let expectedBaggage = builder.put(key: "key1", value: "value1", metadata: "metadata-key = value; othermetadata")
8671
.put(key: "key2", value: "value2")
8772
.put(key: "key3", value: "value3")
@@ -106,6 +91,6 @@ class W3BaggagePropagatorTest: XCTestCase {
10691

10792
let expected1 = ["baggage": "meta=meta-value;somemetadata; someother=foo,nometa=nometa-value"]
10893
let expected2 = ["baggage": "nometa=nometa-value,meta=meta-value;somemetadata; someother=foo"]
109-
XCTAssert( carrier == expected1 || carrier == expected2)
94+
XCTAssert(carrier == expected1 || carrier == expected2)
11095
}
11196
}

Tests/OpenTelemetryApiTests/Context/Propagation/B3PropagatorTests.swift renamed to Tests/OpenTelemetryApiTests/Trace/Propagation/B3PropagatorTests.swift

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// limitations under the License.
1414
//
1515

16-
import OpenTelemetryApi
16+
@testable import OpenTelemetryApi
1717
import XCTest
1818

1919
class B3PropagatorTests: XCTestCase {
@@ -31,21 +31,6 @@ class B3PropagatorTests: XCTestCase {
3131
let setter = TestSetter()
3232
let getter = TestGetter()
3333

34-
struct TestSetter: Setter {
35-
func set(carrier: inout [String: String], key: String, value: String) {
36-
carrier[key] = value
37-
}
38-
}
39-
40-
struct TestGetter: Getter {
41-
func get(carrier: [String: String], key: String) -> [String]? {
42-
if let value = carrier[key] {
43-
return [value]
44-
}
45-
return nil
46-
}
47-
}
48-
4934
override func setUp() {
5035
traceId = TraceId(fromHexString: traceIdHexString)
5136
traceIdShort = TraceId(fromHexString: traceIdShortHexString)

0 commit comments

Comments
 (0)