Skip to content

Commit e4244bd

Browse files
authored
Add W3C Trace Context propagation (#949)
* Add W3C Trace Context propagation * Update documentation * Check identifier-scheme for W3C propagation
1 parent a8f789f commit e4244bd

File tree

6 files changed

+160
-6
lines changed

6 files changed

+160
-6
lines changed

core/kamon-core-tests/src/test/scala/kamon/context/HttpPropagationSpec.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class HttpPropagationSpec extends WordSpec with Matchers with OptionValues {
113113
|entries.outgoing.string = "kamon.context.HttpPropagationSpec$StringEntryCodec"
114114
|
115115
""".stripMargin
116-
).withFallback(ConfigFactory.load().getConfig("kamon.propagation")))
116+
).withFallback(ConfigFactory.load().getConfig("kamon.propagation")), identifierScheme = "single")
117117

118118

119119
def headerReaderFromMap(map: Map[String, String]): HttpPropagation.HeaderReader = new HttpPropagation.HeaderReader {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package kamon.trace
2+
3+
import kamon.context.{Context, HttpPropagation}
4+
import kamon.trace.Trace.SamplingDecision
5+
import org.scalatest.{Matchers, OptionValues, WordSpecLike}
6+
7+
import scala.collection.mutable
8+
9+
class W3CTraceContextSpanPropagationSpec extends WordSpecLike with Matchers with OptionValues {
10+
val traceContextPropagation = SpanPropagation.W3CTraceContext()
11+
12+
"The TraceContext Span propagation for HTTP" should {
13+
"write the Span data into headers" in {
14+
val headersMap = mutable.Map.empty[String, String]
15+
traceContextPropagation.write(testContext(), headerWriterFromMap(headersMap))
16+
17+
headersMap.get("traceparent").value shouldBe "00-00000000000000000000000001020304-0000000004030201-01"
18+
headersMap.get("tracestate").value shouldBe ""
19+
}
20+
21+
"not inject anything if there is no Span in the Context" in {
22+
val headersMap = mutable.Map.empty[String, String]
23+
traceContextPropagation.write(Context.Empty, headerWriterFromMap(headersMap))
24+
headersMap.values shouldBe empty
25+
}
26+
27+
"extract a RemoteSpan from incoming headers when all fields are set" in {
28+
val headersMap = Map(
29+
"traceparent" -> "00-00000000000000000000000001020304-0000000004030201-01",
30+
"tracestate" -> "2222"
31+
)
32+
33+
val spanContext = traceContextPropagation.read(headerReaderFromMap(headersMap), Context.Empty).get(Span.Key)
34+
spanContext.id.string shouldBe "00000000000000000000000001020304"
35+
spanContext.parentId.string shouldBe "0000000004030201"
36+
spanContext.trace.id.string shouldBe "00000000000000000000000001020304"
37+
spanContext.trace.samplingDecision shouldBe SamplingDecision.Sample
38+
}
39+
}
40+
41+
def headerWriterFromMap(map: mutable.Map[String, String]): HttpPropagation.HeaderWriter = new HttpPropagation.HeaderWriter {
42+
override def write(header: String, value: String): Unit = map.put(header, value)
43+
}
44+
45+
def headerReaderFromMap(map: Map[String, String]): HttpPropagation.HeaderReader = new HttpPropagation.HeaderReader {
46+
override def read(header: String): Option[String] = {
47+
if(map.contains("fail"))
48+
sys.error("failing on purpose")
49+
50+
map.get(header)
51+
}
52+
53+
override def readAll(): Map[String, String] = map
54+
}
55+
56+
def testContext(): Context =
57+
Context.of(Span.Key, Span.Remote(
58+
id = Identifier("4321", Array[Byte](4, 3, 2, 1)),
59+
parentId = Identifier("2222", Array[Byte](2, 2, 2, 2)),
60+
trace = Trace(
61+
id = Identifier("1234", Array[Byte](1, 2, 3, 4)),
62+
samplingDecision = SamplingDecision.Sample
63+
)
64+
))
65+
66+
def testContextWithoutParent(): Context =
67+
Context.of(Span.Key, Span.Remote(
68+
id = Identifier("4321", Array[Byte](4, 3, 2, 1)),
69+
parentId = Identifier.Empty,
70+
trace = Trace(
71+
id = Identifier("1234", Array[Byte](1, 2, 3, 4)),
72+
samplingDecision = SamplingDecision.Sample
73+
)
74+
))
75+
}

core/kamon-core/src/main/resources/reference.conf

+2
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ kamon {
315315
# - b3 for the B3 format (see https://github.com/openzipkin/b3-propagation)
316316
# - b3-single for the Zipkin B3 single header format (see https://github.com/openzipkin/b3-propagation#single-header)
317317
# - uber for the Uber/Jaeger format (see https://www.jaegertracing.io/docs/1.16/client-libraries/#propagation-format)
318+
# - w3c for the W3C Trace Context format (see https://www.w3.org/TR/trace-context-1/) - requires identifier-scheme = double
318319
#
319320
span = "b3"
320321
}
@@ -328,6 +329,7 @@ kamon {
328329
# - b3 for the B3 format (see https://github.com/openzipkin/b3-propagation)
329330
# - b3-single for the Zipkin B3 single header format (see https://github.com/openzipkin/b3-propagation#single-header)
330331
# - uber for the Uber/Jaeger format (see https://www.jaegertracing.io/docs/1.16/client-libraries/#propagation-format)
332+
# - w3c for the W3C Trace Context format (see https://www.w3.org/TR/trace-context-1/) - requires identifier-scheme = double
331333
#
332334
span = "b3"
333335
}

core/kamon-core/src/main/scala/kamon/ContextPropagation.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,10 @@ object ContextPropagation {
8989
val propagationConfig = config.getConfig("kamon.propagation")
9090
val httpChannelsConfig = propagationConfig.getConfig("http").configurations
9191
val binaryChannelsConfig = propagationConfig.getConfig("binary").configurations
92+
val identifierScheme = config.getString("kamon.trace.identifier-scheme")
9293

9394
val httpChannels = httpChannelsConfig.map {
94-
case (channelName, channelConfig) => (channelName -> HttpPropagation.from(channelConfig))
95+
case (channelName, channelConfig) => (channelName -> HttpPropagation.from(channelConfig, identifierScheme))
9596
}
9697

9798
val binaryChannels = binaryChannelsConfig.map {

core/kamon-core/src/main/scala/kamon/context/HttpPropagation.scala

+10-4
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ object HttpPropagation {
6464
/**
6565
* Create a new HTTP propagation instance from the provided configuration.
6666
*/
67-
def from(config: Config): Propagation[HttpPropagation.HeaderReader, HttpPropagation.HeaderWriter] = {
68-
new HttpPropagation.Default(Settings.from(config))
67+
def from(propagationConfig: Config, identifierScheme: String): Propagation[HttpPropagation.HeaderReader, HttpPropagation.HeaderWriter] = {
68+
new HttpPropagation.Default(Settings.from(propagationConfig, identifierScheme))
6969
}
7070

7171
/**
@@ -226,16 +226,22 @@ object HttpPropagation {
226226
private val readerWriterShortcuts: Map[String, ReaderWriter] = Map(
227227
"span/b3" -> SpanPropagation.B3(),
228228
"span/uber" -> SpanPropagation.Uber(),
229-
"span/b3-single" -> SpanPropagation.B3Single()
229+
"span/b3-single" -> SpanPropagation.B3Single(),
230+
"span/w3c" -> SpanPropagation.W3CTraceContext()
230231
)
231232

232-
def from(config: Config): Settings = {
233+
def from(config: Config, identifierScheme: String): Settings = {
233234
def buildInstances[ExpectedType : ClassTag](mappings: Map[String, String]): Map[String, ExpectedType] = {
234235
val instanceMap = Map.newBuilder[String, ExpectedType]
235236

236237
mappings.foreach {
237238
case (contextKey, componentClass) =>
238239
val shortcut = s"$contextKey/$componentClass".toLowerCase()
240+
241+
if (componentClass == "span/w3c" && identifierScheme != "double") {
242+
log.warn("W3C TraceContext propagation should be used only with identifier-scheme = double")
243+
}
244+
239245
readerWriterShortcuts.get(shortcut).fold({
240246
try {
241247
instanceMap += (contextKey -> ClassLoading.createInstance[ExpectedType](componentClass, Nil))

core/kamon-core/src/main/scala/kamon/trace/SpanPropagation.scala

+70
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,76 @@ object SpanPropagation {
4040

4141
import Util._
4242

43+
/**
44+
* Reads and Writes a Span instance using the W3C Trace Context propagation format.
45+
* The specification can be found here: https://www.w3.org/TR/trace-context-1/
46+
*/
47+
class W3CTraceContext extends Propagation.EntryReader[HeaderReader] with Propagation.EntryWriter[HeaderWriter] {
48+
import W3CTraceContext._
49+
50+
override def read(medium: HeaderReader, context: Context): Context = {
51+
val contextWithParent = for {
52+
traceParent <- medium.read(Headers.TraceParent)
53+
span <- decodeTraceParent(traceParent)
54+
} yield {
55+
val traceState = medium.read(Headers.TraceState).getOrElse("")
56+
context.withEntry(Span.Key, span).withEntry(TraceStateKey, traceState)
57+
}
58+
59+
contextWithParent.getOrElse(context)
60+
}
61+
62+
override def write(context: Context, medium: HeaderWriter): Unit = {
63+
val span = context.get(Span.Key)
64+
65+
if (span != Span.Empty) {
66+
medium.write(Headers.TraceParent, encodeTraceParent(span))
67+
medium.write(Headers.TraceState, context.get(TraceStateKey))
68+
}
69+
}
70+
}
71+
72+
object W3CTraceContext {
73+
val Version: String = "00"
74+
val TraceStateKey: Context.Key[String] = Context.key("tracestate", "")
75+
76+
object Headers {
77+
val TraceParent = "traceparent"
78+
val TraceState = "tracestate"
79+
}
80+
81+
def apply(): W3CTraceContext =
82+
new W3CTraceContext()
83+
84+
def decodeTraceParent(traceParent: String): Option[Span] = {
85+
val identityProvider = Identifier.Scheme.Double
86+
87+
def unpackSamplingDecision(decision: String): SamplingDecision =
88+
if ("01" == decision) SamplingDecision.Sample else SamplingDecision.Unknown
89+
90+
val traceParentComponents = traceParent.split("-")
91+
92+
if (traceParentComponents.length != 4) None else {
93+
val traceID = identityProvider.traceIdFactory.from(traceParentComponents(1))
94+
val spanID = identityProvider.spanIdFactory.from(traceParentComponents(2))
95+
val samplingDecision = unpackSamplingDecision(traceParentComponents(3))
96+
97+
Some(Span.Remote(traceID, spanID, Trace(traceID, samplingDecision)))
98+
}
99+
}
100+
101+
def encodeTraceParent(parent: Span): String = {
102+
def idToHex(identifier: Identifier, length: Int): String = {
103+
val leftPad = (string: String) => "0" * (length - string.length) + string
104+
leftPad(identifier.bytes.map("%02X" format _).mkString)
105+
}
106+
107+
val samplingDecision = if (parent.trace.samplingDecision == SamplingDecision.Sample) "01" else "00"
108+
109+
s"$Version-${idToHex(parent.trace.id, 32)}-${idToHex(parent.id, 16)}-${samplingDecision}"
110+
}
111+
}
112+
43113
/**
44114
* Reads and Writes a Span instance using the B3 propagation format. The specification and semantics of the B3
45115
* Propagation format can be found here: https://github.com/openzipkin/b3-propagation

0 commit comments

Comments
 (0)