From 5a488bd6e33c0cc129bff111bb653c82eb006cf9 Mon Sep 17 00:00:00 2001 From: dst4096 Date: Tue, 8 Oct 2019 00:40:02 +0300 Subject: [PATCH] Fix for #2114 Support b3 trace header. With this fix Linkerd will propagate b3 single header the same way as x-b3- multi headers. If a request contains b3 single header and X-B3- multi headers, the trace context from b3 is preferred and x-b3- will be ignored. Signed-off-by: dst4096 --- .../linkerd/http/ZipkinTracePropagator.scala | 152 +++++++-- .../h2/ZipkinTracePropagatorTest.scala | 303 +++++++++++++++++- .../http/ZipkinTracePropagatorTest.scala | 296 ++++++++++++++++- 3 files changed, 728 insertions(+), 23 deletions(-) diff --git a/linkerd/protocol/base-http/src/main/scala/io/buoyant/linkerd/http/ZipkinTracePropagator.scala b/linkerd/protocol/base-http/src/main/scala/io/buoyant/linkerd/http/ZipkinTracePropagator.scala index 55d4b48373..bb953420e7 100644 --- a/linkerd/protocol/base-http/src/main/scala/io/buoyant/linkerd/http/ZipkinTracePropagator.scala +++ b/linkerd/protocol/base-http/src/main/scala/io/buoyant/linkerd/http/ZipkinTracePropagator.scala @@ -7,7 +7,6 @@ import com.twitter.finagle.tracing.{Flags, SpanId, TraceId, TraceId128} import com.twitter.util.Try import io.buoyant.linkerd.{TracePropagator, TracePropagatorInitializer} import io.buoyant.router.http.{HeadersLike, RequestLike} -import com.twitter.logging.Logger class ZipkinTracePropagator[Req, H: HeadersLike](implicit requestLike: RequestLike[Req, H]) extends TracePropagator[Req] { /** @@ -21,7 +20,7 @@ class ZipkinTracePropagator[Req, H: HeadersLike](implicit requestLike: RequestLi /** * Return a sampler which decides if the given request should be sampled, based on properties - * of the request (zipkin or linkerd if zipkin not present). If None is returned, the decision of whether to sample the request is deferred + * of the request (zipkin). If None is returned, the decision of whether to sample the request is deferred * to the tracer. */ override def sampler(req: Req): Option[Sampler] = { @@ -49,9 +48,66 @@ object ZipkinTrace { val ZipkinSampleHeader = "x-b3-sampled" val ZipkinFlagsHeader = "x-b3-flags" - def get[H: HeadersLike](headers: H): Option[TraceId] = { - val headersLike = implicitly[HeadersLike[H]] + /** + * The separate "b3" header in b3 single header format : + * b3={x-b3-traceid}-{x-b3-spanid}-{if x-b3-flags 'd' else x-b3-sampled}-{x-b3-parentspanid}, + * where the last two fields are optional. + */ + val ZipkinB3SingleHeader = "b3" + + /** + * Extract a traceid from the Zipkin b3 single header. If no traceid try returning the sampled/flags info. + * @note A traceid cannot be constructed if no spanid is present. + * @param b3SingleHeader + * @tparam H + * @return A tuple of three values (traceid, sampled, flags) + */ + def getFromB3SingleHeader[H: HeadersLike](b3SingleHeader: String): (Option[TraceId], Option[Boolean], Flags) = { + /* extract from the b3 single header the values of sampled and flags which closely + * match the behavior from X-B3 multi headers, X-B3-Sampled and X-B3-Flags */ + def matchSampledAndFlags(value: String): (Option[Boolean], Flags) = { + value match { + case "0" => (Option(false), Flags()) + case "d" => (None, Flags(1)) + case "1" => (Option(true), Flags()) + case _ => (None, Flags()) + } + } + + b3SingleHeader.split("-").toList match { + case sampled :: Nil => + // only debug flag or sampled + val (sampled_, flags) = matchSampledAndFlags(sampled) + (None, sampled_, flags) + + case traceId :: spanId :: Nil => + // expect to read a 128bit traceid field, b3 single header supports 128bit traceids + val trace128Bit = TraceId128(traceId) + val (sampled_, flags) = matchSampledAndFlags("") + (SpanId.fromString(spanId).map(sid => TraceId(trace128Bit.low, None, sid, None, Flags(), trace128Bit.high)), sampled_, flags) + + case traceId :: spanId :: sampled :: Nil => + // expect to read a 128bit traceid field, b3 single header supports 128bit traceids + + val trace128Bit = TraceId128(traceId) + val (sampled_, flags) = matchSampledAndFlags(sampled) + + (SpanId.fromString(spanId).map(sid => TraceId(trace128Bit.low, None, sid, sampled_, flags, trace128Bit.high)), sampled_, flags) + + case traceId :: spanId :: sampled :: parentId :: Nil => + // expect to read a 128bit traceid field, b3 single header supports 128bit traceids + val trace128Bit = TraceId128(traceId) + val (sampled_, flags) = matchSampledAndFlags(sampled) + + (SpanId.fromString(spanId).map(sid => TraceId(trace128Bit.low, SpanId.fromString(parentId), sid, sampled_, flags, trace128Bit.high)), sampled_, flags) + + case _ => + // bogus, do not handle the case when b3 is empty or has more than 4 components + (None, None, Flags()) + } + } + def getFromXB3MultiHeaders[H: HeadersLike](headers: H): Option[TraceId] = { // expect to read a 128bit traceid field, b3 single header supports 128bit traceids val trace128Bit = caseInsensitiveGet(headers, ZipkinTraceHeader) match { case Some(s) => TraceId128(s) @@ -71,9 +127,45 @@ object ZipkinTrace { } } + /** + * Get a trace id from the request, if it has one, in either the historical multiple "x-b3-" headers or the + * newer "b3" single header. The "b3" single-header variant takes precedence over the multiple header one + * when extracting fields, that also implies ignoring the latter if "b3" single header is read. + * + * @note This method does not touches the request, b3 header is not remove, x-b3- headers not removed + * @note The "b3" encodings specs, single or multi: https://github.com/openzipkin/b3-propagation#http-encodings + * @param headers + * @return Option[TraceId] + */ + def get[H: HeadersLike](headers: H): Option[TraceId] = { + val headersLike = implicitly[HeadersLike[H]] + + caseInsensitiveGet(headers, ZipkinB3SingleHeader) match { + case Some(v) => { + val (traceId, sampled, flags) = getFromB3SingleHeader(v) + traceId + } + case _ => getFromXB3MultiHeaders(headers) + } + } + + /** + * There's no way to know what the downstream is capable of, and it is more likely it supports "X-B3-*" vs not, so, + * the portable choice is to always write down "X-B3-" even if we read "b3". Finagle does a similar thing in + * finagle-http-base: read "b3", write back "X-B3-". + * + * @note This method does not touches the request, b3 header is not remove, x-b3- headers not removed + * @note The "b3" encodings specs, single or multi: https://github.com/openzipkin/b3-propagation#http-encodings + * @param headers + * @return Option[TraceId] + */ + def set[H: HeadersLike](headers: H, id: TraceId): Unit = { val headersLike = implicitly[HeadersLike[H]] + // remove any b3 single header, if present, before setting x-b3 multi headers + val _ = headersLike.remove(headers, ZipkinB3SingleHeader) + headersLike.set(headers, ZipkinSpanHeader, id.spanId.toString) // support setting a 128bit traceid @@ -97,6 +189,7 @@ object ZipkinTrace { case None => // do nothing } } + () } @@ -106,24 +199,43 @@ object ZipkinTrace { val samplerTrue: Option[Float] = Option(1.0f) val samplerFalse: Option[Float] = Option(0.0f) - // first try getting x-b3-flags, flags = 1 means debug - val flags = caseInsensitiveGet(headers, ZipkinFlagsHeader) - if (flags.isEmpty) { - // try getting x-b3-sampled only if x-b3-flags not present - caseInsensitiveGet(headers, ZipkinSampleHeader).flatMap { s => - Try(s.toFloat).toOption match { - //x-b3-sampled present, the only valid values for x-b3-sampled are 0 and 1, any other values are invalid and should be ignored - case Some(v) if v == 0 => samplerFalse - case Some(v) if v == 1 => samplerTrue - case _ => samplerNone + // first try getting flags/sampled info from b3 single header + caseInsensitiveGet(headers, ZipkinB3SingleHeader) match { + case Some(v) => { + val (traceId, sampled, flags) = getFromB3SingleHeader(v) + flags match { + case Flags(1) => samplerTrue // flags debug + case _ => { // flags invalid or not present, look for sampled field + sampled match { + case Some(true) => samplerTrue + case Some(false) => samplerFalse + case _ => samplerNone + } + } } } - } else { - flags.flatMap { s => - Try(s.toLong).toOption match { - //x-b3-flags present, the only valid value for x-b3-flags is 1, any other values are invalid and should be ignored - case Some(v) if v == 1 => samplerTrue - case _ => samplerNone + case _ => { + // fallback to getting flags/sampled info from x-b3 headers in case b3 not present + // first try getting x-b3-flags, flags = 1 means debug + val flags = caseInsensitiveGet(headers, ZipkinFlagsHeader) + if (flags.isEmpty) { + // try getting x-b3-sampled only if x-b3-flags not present + caseInsensitiveGet(headers, ZipkinSampleHeader).flatMap { s => + Try(s.toFloat).toOption match { + //x-b3-sampled present, the only valid values for x-b3-sampled are 0 and 1, any other values are invalid and should be ignored + case Some(v) if v == 0 => samplerFalse + case Some(v) if v == 1 => samplerTrue + case _ => samplerNone + } + } + } else { + flags.flatMap { s => + Try(s.toLong).toOption match { + //x-b3-flags present, the only valid value for x-b3-flags is 1, any other values are invalid and should be ignored + case Some(v) if v == 1 => samplerTrue + case _ => samplerNone + } + } } } } diff --git a/linkerd/protocol/h2/src/test/scala/io/buoyant/linkerd/protocol/h2/ZipkinTracePropagatorTest.scala b/linkerd/protocol/h2/src/test/scala/io/buoyant/linkerd/protocol/h2/ZipkinTracePropagatorTest.scala index 5d6b97cf65..821542563e 100644 --- a/linkerd/protocol/h2/src/test/scala/io/buoyant/linkerd/protocol/h2/ZipkinTracePropagatorTest.scala +++ b/linkerd/protocol/h2/src/test/scala/io/buoyant/linkerd/protocol/h2/ZipkinTracePropagatorTest.scala @@ -38,7 +38,7 @@ class ZipkinTracePropagatorTest extends FunSuite { assert(tid.parentId.toString().equals("e457b5a2e4d86bd1")) assert(tid.sampled.contains(true)) - // expect to get the right sampled value which is 0 + // expect to get the right sampled value which is 1 assert(ZipkinTrace.getSampler(req1.headers).contains(1.0f)) }} @@ -81,7 +81,7 @@ class ZipkinTracePropagatorTest extends FunSuite { //flags 0 (invalid value), no sampled => sampler None req.headers.remove("x-b3-flags") req.headers.add("x-b3-flags", "0") - assert(ZipkinTrace.getSampler(req.headers).contains(0.0f)) + assert(ZipkinTrace.getSampler(req.headers).isEmpty) //flags asd (invalid value), no sampled = > sampler None req.headers.remove("x-b3-flags") @@ -111,4 +111,303 @@ class ZipkinTracePropagatorTest extends FunSuite { req.headers.remove("x-b3-sampled") assert(ZipkinTrace.getSampler(req.headers).isEmpty) } + + test("get traceid from a b3 single header - empty header") { + // b3: + val ztp = new ZipkinTracePropagator() + val req = Request("http", Method.Get, "auf", "/", Stream.empty()) + req.headers.add("b3", "") + + val trace = ztp.traceId(req) + // this should not have returned a traceId because there's no span + assert(trace.isEmpty) + //b3 has not been removed, other x-b3 have not been added + assert(req.headers.contains("b3")) + + val sampler = ztp.sampler(req) + // no sampler should be returned + assert(sampler.isEmpty) + } + + test("get traceid from a b3 single header - one field - don't sample - b3: 0") { + //b3: 0 + val ztp = new ZipkinTracePropagator() + val req = Request("http", Method.Get, "auf", "/", Stream.empty()) + req.headers.add("b3", "0") + + val trace = ztp.traceId(req) + // this should not have returned a traceId because there's no span + assert(trace.isEmpty) + //b3 has not been removed, other x-b3 have not been added + assert(req.headers.contains("b3")) + + // expect to get the right sampled value which is 0 + val sampler = ZipkinTrace.getSampler(req.headers) + assert(sampler.contains(0.0f)) + } + + test("get traceid from a b3 single header - one field - sampled - b3: 1") { + //b3: 1 + val ztp = new ZipkinTracePropagator() + val req = Request("http", Method.Get, "auf", "/", Stream.empty()) + req.headers.add("b3", "1") + + val trace = ztp.traceId(req) + // this should not have returned a traceId because there's no span + assert(trace.isEmpty) + //b3 has not been removed, other x-b3 have not been added + assert(req.headers.contains("b3")) + + // expect to get the right sampled value which is 1 + val sampler = ZipkinTrace.getSampler(req.headers) + assert(sampler.contains(1.0f)) + } + + test("get traceid from a b3 single header - one field - debug - b3: d") { + //b3: d + val ztp = new ZipkinTracePropagator() + val req = Request("http", Method.Get, "auf", "/", Stream.empty()) + req.headers.add("b3", "d") + + val trace = ztp.traceId(req) + // this should not have returned a traceId because there's no span + assert(trace.isEmpty) + //b3 has not been removed, other x-b3 have not been added + assert(req.headers.contains("b3")) + + // expect to get the right sampled value which is 1 when debug is set + val sampler = ZipkinTrace.getSampler(req.headers) + assert(sampler.contains(1.0f)) + } + + test("get traceid from a b3 single header - one field - invalid - b3: 2") { + //b3: d + val ztp = new ZipkinTracePropagator() + val req = Request("http", Method.Get, "auf", "/", Stream.empty()) + req.headers.add("b3", "s") + + val trace = ztp.traceId(req) + // this should not have returned a traceId because there's no span + assert(trace.isEmpty) + //b3 has not been removed, other x-b3 have not been added + assert(req.headers.contains("b3")) + + // expect to not get a sampler when sampling bit is invalid value (other than 0/1/d) + val sampler = ZipkinTrace.getSampler(req.headers) + assert(sampler.isEmpty) + } + + test("get traceid from a b3 single header - two fields - not yet sampled root span") { + //b3: a3ce929d0e0e4736-00f067aa0ba902b7 + val ztp = new ZipkinTracePropagator() + val req = Request("http", Method.Get, "auf", "/", Stream.empty()) + req.headers.add("b3", "a3ce929d0e0e4736-00f067aa0ba902b7") + + val trace = ztp.traceId(req) + //b3 has not been removed, other x-b3 have not been added + assert(req.headers.contains("b3")) + + assert(trace.isDefined) //expect trace exists + trace.foreach { tid => { + assert(tid.traceId.toString().equals("a3ce929d0e0e4736")) + assert(tid.spanId.toString().equals("00f067aa0ba902b7")) + + // expect to not get a sampler when sampling bit is not set + val sampler = ZipkinTrace.getSampler(req.headers) + assert(sampler.isEmpty) + + ztp.setContext(req, tid) + // check "b3" has been removed, "x-b3-traceid" and "x-b3-spanid" have been added to request + assert(req.headers.get("b3").isEmpty) + assert(req.headers.contains("x-b3-traceid")) + assert(req.headers.contains("x-b3-spanid")) + }} + + // after "x-b3-" have been added check they have expected values + val trace2 = ztp.traceId(req) + assert(trace == trace2) + } + + test("get traceid from a b3 single header - three fields - sampled root span") { + //b3: a3ce929d0e0e4736-00f067aa0ba902b7-1 + val ztp = new ZipkinTracePropagator() + val req = Request("http", Method.Get, "auf", "/", Stream.empty()) + req.headers.add("b3", "a3ce929d0e0e4736-00f067aa0ba902b7-1") + + val trace = ztp.traceId(req) + //b3 has not been removed, other x-b3 have not been added + assert(req.headers.contains("b3")) + + assert(trace.isDefined) //expect trace exists + trace.foreach { tid => { + assert(tid.traceId.toString().equals("a3ce929d0e0e4736")) + assert(tid.spanId.toString().equals("00f067aa0ba902b7")) + assert(tid.sampled.contains(true)) + + // expect to get the right sampled value which is 1 + val sampler = ZipkinTrace.getSampler(req.headers) + assert(sampler.contains(1.0f)) + + ztp.setContext(req, tid) + // check "b3" has been removed, "x-b3-traceid" and "x-b3-spanid" and "x-b3-sampledid" have been added to request + assert(req.headers.get("b3").isEmpty) + assert(req.headers.contains("x-b3-traceid")) + assert(req.headers.contains("x-b3-spanid")) + assert(req.headers.contains("x-b3-sampled")) + }} + + // after "x-b3-" have been added check they have the same values as above + val trace2 = ztp.traceId(req) + assert(trace == trace2) + } + + test("get traceid from a b3 single header - four fields - child span on debug") { + //b3: a3ce929d0e0e4736-00f067aa0ba902b7-d-5b4185666d50f68b + + val ztp = new ZipkinTracePropagator() + val req = Request("http", Method.Get, "auf", "/", Stream.empty()) + req.headers.add("b3", "a3ce929d0e0e4736-00f067aa0ba902b7-d-5b4185666d50f68b") + + val trace = ztp.traceId(req) + //b3 has not been removed, other x-b3 have not been added + assert(req.headers.contains("b3")) + + assert(trace.isDefined) //expect trace exists + trace.foreach { tid => { + assert(tid.traceId.toString().equals("a3ce929d0e0e4736")) + assert(tid.spanId.toString().equals("00f067aa0ba902b7")) + assert(tid.parentId.toString().equals("5b4185666d50f68b")) + assert(tid.flags.toLong == 1) + + // expect to get the right sampled value which is 1 + val sampler = ZipkinTrace.getSampler(req.headers) + assert(sampler.contains(1.0f)) + + ztp.setContext(req, tid) + // check "b3" has been removed, "x-b3-traceid" and "x-b3-spanid" and "x-b3-flags" have been added to request + assert(req.headers.get("b3").isEmpty) + // check sampled not set when debug flag set + assert(req.headers.get("x-b3-sampled").isEmpty) + assert(req.headers.contains("x-b3-traceid")) + assert(req.headers.contains("x-b3-spanid")) + assert(req.headers.contains("x-b3-flags")) + }} + + //test x-b3- headers have been added so we can get the same trace from them + val trace2 = ztp.traceId(req) + assert(trace == trace2) + } + + test("get traceid from a b3 single header - four fields - not sampled child span") { + //b3: a3ce929d0e0e4736-00f067aa0ba902b7-d-5b4185666d50f68b + + val ztp = new ZipkinTracePropagator() + val req = Request("http", Method.Get, "auf", "/", Stream.empty()) + req.headers.add("b3", "a3ce929d0e0e4736-00f067aa0ba902b7-0-5b4185666d50f68b") + + val trace = ztp.traceId(req) + //b3 has not been removed, other x-b3 have not been added + assert(req.headers.contains("b3")) + + assert(trace.isDefined) //expect trace exists + trace.foreach { tid => { + assert(tid.traceId.toString().equals("a3ce929d0e0e4736")) + assert(tid.spanId.toString().equals("00f067aa0ba902b7")) + assert(tid.parentId.toString().equals("5b4185666d50f68b")) + assert(tid.sampled.contains(false)) + + // expect to get the right sampled value which is 0 + val sampler = ZipkinTrace.getSampler(req.headers) + assert(sampler.contains(0.0f)) + + ztp.setContext(req, tid) + // check "b3" has been removed, "x-b3-traceid" and "x-b3-spanid" and "x-b3-parentspanid" have been added to request + assert(req.headers.get("b3").isEmpty) + // check sampled not set when debug flag set + assert(req.headers.contains("x-b3-traceid")) + assert(req.headers.contains("x-b3-spanid")) + assert(req.headers.contains("x-b3-parentspanid")) + assert(req.headers.contains("x-b3-sampled")) + }} + + //test x-b3- headers have been added so we can get the same trace from them + val trace2 = ztp.traceId(req) + assert(trace == trace2) + } + + test("get traceid from a b3 single header - 128bit trace, two fields") { + //b3: 80f198ee56343ba864fe8b2a57d3eff7-05e3ac9a4f6e3b90 + val ztp = new ZipkinTracePropagator() + val req = Request("http", Method.Get, "auf", "/", Stream.empty()) + req.headers.add("b3", "80f198ee56343ba864fe8b2a57d3eff7-05e3ac9a4f6e3b90") + + val trace = ztp.traceId(req) + //b3 has not been removed + assert(!req.headers.get("b3").isEmpty) + // check "x-b3-traceid" and "x-b3-spanid" have been added to request + //assert(req.headers.toSeq.toSet == Set("x-b3-traceid", "x-b3-spanid")) + + assert(trace.isDefined) //expect trace exists + trace.foreach { tid => { + assert(tid.traceId.toString().equals("64fe8b2a57d3eff7")) + assert(tid.spanId.toString().equals("05e3ac9a4f6e3b90")) + assert(tid.traceIdHigh.toString().contains("80f198ee56343ba8)")) + }} + + // after "x-b3-" have been added check they have expected values + val trace2 = ztp.traceId(req) + assert(trace == trace2) + } + + + test("b3 single headers preferred over x-b3- multi headers") { + val ztp = new ZipkinTracePropagator() + val req = Request("http", Method.Get, "auf", "/", Stream.empty()) + req.headers.add("b3", "a3ce929d0e0e4736-00f067aa0ba902b7-1") + req.headers.add("x-b3-traceid", "0000000000000001") + req.headers.add("x-b3-spanid", "0000000000000002") + req.headers.add("x-b3-sampled", "0") + + val trace = ztp.traceId(req) + assert(trace.isDefined) //expect trace exists + trace.foreach { tid => { + assert(tid.traceId.toString().equals("a3ce929d0e0e4736")) //expect traceid from b3 not from x-b3-traceid) + assert(tid.spanId.toString().equals("00f067aa0ba902b7")) // expect spanid from b3 not from x-b3-spanid + assert(tid.sampled.contains(true)) // expect samplef from b3 not from x-b3-sampled + }} + } + + test("same trace from b3 single headers and x-b3- multi headers, 128bit traceid, UPPER CASE header don't matter") { + /* Turn on tracing and see if + b3=80f198ee56343ba864fe8b2a57d3eff7-05e3ac9a4f6e3b90-1-e457b5a2e4d86bd1 results in the same context as: + X-B3-TraceId: 80f198ee56343ba864fe8b2a57d3eff7 + X-B3-ParentSpanId: 05e3ac9a4f6e3b90 + X-B3-SpanId: e457b5a2e4d86bd1 + X-B3-Sampled: 1 + */ + + //NOTE: This will also test case doesn't matter for b3 single or x-b3- multi headers + val ztp = new ZipkinTracePropagator() + val req1 = Request("http", Method.Get, "auf", "/", Stream.empty()) + req1.headers.add("B3", "80f198ee56343ba864fe8b2a57d3eff7-05e3ac9a4f6e3b90-1-e457b5a2e4d86bd1") + + val req2 = Request("http", Method.Get, "auf", "/", Stream.empty()) + req2.headers.add("X-B3-TRACEID", "80f198ee56343ba864fe8b2a57d3eff7") + req2.headers.add("X-B3-SPANID", "05e3ac9a4f6e3b90") + req2.headers.add("X-B3-SAMPLED", "1") + req2.headers.add("X-B3-PARENTSPANID", "e457b5a2e4d86bd1") + + val trace1 = ztp.traceId(req1) + val trace2 = ztp.traceId(req2) + + assert(trace1 == trace2) + } + + test("cannot get trace from invalid b3 single header, too many fields") { + val ztp = new ZipkinTracePropagator() + val req1 = Request("http", Method.Get, "auf", "/", Stream.empty()) + req1.headers.add("B3", "80f198ee56343ba864fe8b2a57d3eff7-05e3ac9a4f6e3b90-1-e457b5a2e4d86bd1-extra1") + + assert(ztp.traceId(req1).isEmpty) + } } diff --git a/linkerd/protocol/http/src/test/scala/io/buoyant/linkerd/protocol/http/ZipkinTracePropagatorTest.scala b/linkerd/protocol/http/src/test/scala/io/buoyant/linkerd/protocol/http/ZipkinTracePropagatorTest.scala index 82b30ed952..5ff88c02fd 100644 --- a/linkerd/protocol/http/src/test/scala/io/buoyant/linkerd/protocol/http/ZipkinTracePropagatorTest.scala +++ b/linkerd/protocol/http/src/test/scala/io/buoyant/linkerd/protocol/http/ZipkinTracePropagatorTest.scala @@ -36,6 +36,9 @@ class ZipkinTracePropagatorTest extends FunSuite { assert(tid.spanId.toString().equals("05e3ac9a4f6e3b90")) assert(tid.parentId.toString().equals("e457b5a2e4d86bd1")) assert(tid.sampled.contains(true)) + + // expect to get the right sampled value which is 1 + assert(ZipkinTrace.getSampler(req1.headerMap).contains(1.0f)) }} // expect to get the right sampled value which is 1 @@ -77,7 +80,7 @@ class ZipkinTracePropagatorTest extends FunSuite { //flags 0 (invalid value), no sampled => sampler None req.headerMap.remove("x-b3-flags") req.headerMap.add("x-b3-flags", "0") - assert(ZipkinTrace.getSampler(req.headerMap).contains(0.0f)) + assert(ZipkinTrace.getSampler(req.headerMap).isEmpty) //flags asd (invalid value), no sampled = > sampler None req.headerMap.remove("x-b3-flags") @@ -107,4 +110,295 @@ class ZipkinTracePropagatorTest extends FunSuite { req.headerMap.remove("x-b3-sampled") assert(ZipkinTrace.getSampler(req.headerMap).isEmpty) } + + test("get traceid from a b3 single header - empty header") { + // b3: + val ztp = new ZipkinTracePropagator + val req = Request() + req.headerMap.add("b3", "") + + val trace = ztp.traceId(req) + // this should not have returned a traceId because there's no span + assert(trace.isEmpty) + //b3 has not been removed, other x-b3 have not been added + assert(req.headerMap.keys == Set("b3")) + + val sampler = ztp.sampler(req) + // no sampler should be returned + assert(sampler.isEmpty) + } + + test("get traceid from a b3 single header - one field - don't sample - b3: 0") { + //b3: 0 + val ztp = new ZipkinTracePropagator() + val req = Request() + req.headerMap.add("b3", "0") + + val trace = ztp.traceId(req) + // this should not have returned a traceId because there's no span + assert(trace.isEmpty) + //b3 has not been removed, other x-b3 have not been added + assert(req.headerMap.keys == Set("b3")) + + // expect to get the right sampled value which is 0 + val sampler = ZipkinTrace.getSampler(req.headerMap) + assert(sampler.contains(0.0f)) + } + + test("get traceid from a b3 single header - one field - sampled - b3: 1") { + //b3: 1 + val ztp = new ZipkinTracePropagator() + val req = Request() + req.headerMap.add("b3", "1") + + val trace = ztp.traceId(req) + // this should not have returned a traceId because there's no span + assert(trace.isEmpty) + //b3 has not been removed, other x-b3 have not been added + assert(req.headerMap.keys == Set("b3")) + + // expect to get the right sampled value which is 1 + val sampler = ZipkinTrace.getSampler(req.headerMap) + assert(sampler.contains(1.0f)) + } + + test("get traceid from a b3 single header - one field - debug - b3: d") { + //b3: d + val ztp = new ZipkinTracePropagator() + val req = Request() + req.headerMap.add("b3", "d") + + val trace = ztp.traceId(req) + // this should not have returned a traceId because there's no span + assert(trace.isEmpty) + //b3 has not been removed, other x-b3 have not been added + assert(req.headerMap.keys == Set("b3")) + + // expect to get the right sampled value which is 1 when debug is set + val sampler = ZipkinTrace.getSampler(req.headerMap) + assert(sampler.contains(1.0f)) + } + + test("get traceid from a b3 single header - one field - invalid - b3: 2") { + //b3: d + val ztp = new ZipkinTracePropagator() + val req = Request() + req.headerMap.add("b3", "s") + + val trace = ztp.traceId(req) + // this should not have returned a traceId because there's no span + assert(trace.isEmpty) + //b3 has not been removed, other x-b3 have not been added + assert(req.headerMap.keys == Set("b3")) + + // expect to not get a sampler when sampling bit is invalid value (other than 0/1/d) + val sampler = ZipkinTrace.getSampler(req.headerMap) + assert(sampler.isEmpty) + } + + test("get traceid from a b3 single header - two fields - not yet sampled root span") { + //b3: a3ce929d0e0e4736-00f067aa0ba902b7 + val ztp = new ZipkinTracePropagator() + val req = Request() + req.headerMap.add("b3", "a3ce929d0e0e4736-00f067aa0ba902b7") + + val trace = ztp.traceId(req) + //b3 has not been removed, other x-b3 have not been added + assert(req.headerMap.keys == Set("b3")) + + assert(trace.isDefined) //expect trace exists + trace.foreach { tid => { + assert(tid.traceId.toString().equals("a3ce929d0e0e4736")) + assert(tid.spanId.toString().equals("00f067aa0ba902b7")) + + // expect to not get a sampler when sampling bit is not set + val sampler = ZipkinTrace.getSampler(req.headerMap) + assert(sampler.isEmpty) + + ztp.setContext(req, tid) + // check "b3" has been removed, "x-b3-traceid" and "x-b3-spanid" have been added to request + assert(req.headerMap.get("b3").isEmpty) + assert(Set("x-b3-traceid", "x-b3-spanid").subsetOf(req.headerMap.keys.toSet)) + }} + + // after "x-b3-" have been added check they have expected values + val trace2 = ztp.traceId(req) + assert(trace == trace2) + } + + test("get traceid from a b3 single header - three fields - sampled root span") { + //b3: a3ce929d0e0e4736-00f067aa0ba902b7-1 + val ztp = new ZipkinTracePropagator() + val req = Request() + req.headerMap.add("b3", "a3ce929d0e0e4736-00f067aa0ba902b7-1") + + val trace = ztp.traceId(req) + //b3 has not been removed, other x-b3 have not been added + assert(req.headerMap.keys == Set("b3")) + + assert(trace.isDefined) //expect trace exists + trace.foreach { tid => { + assert(tid.traceId.toString().equals("a3ce929d0e0e4736")) + assert(tid.spanId.toString().equals("00f067aa0ba902b7")) + assert(tid.sampled.contains(true)) + + // expect to get the right sampled value which is 1 + val sampler = ZipkinTrace.getSampler(req.headerMap) + assert(sampler.contains(1.0f)) + + ztp.setContext(req, tid) + // check "b3" has been removed, "x-b3-traceid" and "x-b3-spanid" and "x-b3-sampledid" have been added to request + assert(req.headerMap.get("b3").isEmpty) + assert(Set("x-b3-traceid", "x-b3-spanid", "x-b3-sampled").subsetOf(req.headerMap.keys.toSet)) + }} + + // after "x-b3-" have been added check they have the same values as above + val trace2 = ztp.traceId(req) + assert(trace == trace2) + } + + test("get traceid from a b3 single header - four fields - child span on debug") { + //b3: a3ce929d0e0e4736-00f067aa0ba902b7-d-5b4185666d50f68b + + val ztp = new ZipkinTracePropagator() + val req = Request() + req.headerMap.add("b3", "a3ce929d0e0e4736-00f067aa0ba902b7-d-5b4185666d50f68b") + + val trace = ztp.traceId(req) + //b3 has not been removed, other x-b3 have not been added + assert(req.headerMap.keys == Set("b3")) + + assert(trace.isDefined) //expect trace exists + trace.foreach { tid => { + assert(tid.traceId.toString().equals("a3ce929d0e0e4736")) + assert(tid.spanId.toString().equals("00f067aa0ba902b7")) + assert(tid.parentId.toString().equals("5b4185666d50f68b")) + assert(tid.flags.toLong == 1) + + // expect to get the right sampled value which is 1 + val sampler = ZipkinTrace.getSampler(req.headerMap) + assert(sampler.contains(1.0f)) + + ztp.setContext(req, tid) + // check "b3" has been removed, "x-b3-traceid" and "x-b3-spanid" and "x-b3-flags" have been added to request + assert(req.headerMap.get("b3").isEmpty) + // check sampled not set when debug flag set + assert(req.headerMap.get("x-b3-sampled").isEmpty) + assert(Set("x-b3-traceid", "x-b3-spanid", "x-b3-flags").subsetOf(req.headerMap.keys.toSet)) + }} + + //test x-b3- headers have been added so we can get the same trace from them + val trace2 = ztp.traceId(req) + assert(trace == trace2) + } + + test("get traceid from a b3 single header - four fields - not sampled child span") { + //b3: a3ce929d0e0e4736-00f067aa0ba902b7-d-5b4185666d50f68b + + val ztp = new ZipkinTracePropagator() + val req = Request() + req.headerMap.add("b3", "a3ce929d0e0e4736-00f067aa0ba902b7-0-5b4185666d50f68b") + + val trace = ztp.traceId(req) + //b3 has not been removed, other x-b3 have not been added + assert(req.headerMap.keys == Set("b3")) + + assert(trace.isDefined) //expect trace exists + trace.foreach { tid => { + assert(tid.traceId.toString().equals("a3ce929d0e0e4736")) + assert(tid.spanId.toString().equals("00f067aa0ba902b7")) + assert(tid.parentId.toString().equals("5b4185666d50f68b")) + assert(tid.sampled.contains(false)) + + // expect to get the right sampled value which is 0 + val sampler = ZipkinTrace.getSampler(req.headerMap) + assert(sampler.contains(0.0f)) + + ztp.setContext(req, tid) + // check "b3" has been removed, "x-b3-traceid" and "x-b3-spanid" and "x-b3-parentspanid" have been added to request + assert(req.headerMap.get("b3").isEmpty) + // check sampled not set when debug flag set + assert(Set("x-b3-traceid", "x-b3-spanid", "x-b3-parentspanid", "x-b3-sampled").subsetOf(req.headerMap.keys.toSet)) + }} + + //test x-b3- headers have been added so we can get the same trace from them + val trace2 = ztp.traceId(req) + assert(trace == trace2) + } + + test("get traceid from a b3 single header - 128bit trace, two fields") { + //b3: 80f198ee56343ba864fe8b2a57d3eff7-05e3ac9a4f6e3b90 + val ztp = new ZipkinTracePropagator() + val req = Request() + req.headerMap.add("b3", "80f198ee56343ba864fe8b2a57d3eff7-05e3ac9a4f6e3b90") + + val trace = ztp.traceId(req) + //b3 has not been removed + assert(!req.headerMap.get("b3").isEmpty) + // check "x-b3-traceid" and "x-b3-spanid" have been added to request + //assert(req.headerMap.keys == Set("x-b3-traceid", "x-b3-spanid")) + + assert(trace.isDefined) //expect trace exists + trace.foreach { tid => { + assert(tid.traceId.toString().equals("64fe8b2a57d3eff7")) + assert(tid.spanId.toString().equals("05e3ac9a4f6e3b90")) + assert(tid.traceIdHigh.toString().contains("80f198ee56343ba8)")) + }} + + // after "x-b3-" have been added check they have expected values + val trace2 = ztp.traceId(req) + assert(trace == trace2) + } + + + test("b3 single headers preferred over x-b3- multi headers") { + val ztp = new ZipkinTracePropagator() + val req = Request() + req.headerMap.add("b3", "a3ce929d0e0e4736-00f067aa0ba902b7-1") + req.headerMap.add("x-b3-traceid", "0000000000000001") + req.headerMap.add("x-b3-spanid", "0000000000000002") + req.headerMap.add("x-b3-sampled", "0") + + val trace = ztp.traceId(req) + assert(trace.isDefined) //expect trace exists + trace.foreach { tid => { + assert(tid.traceId.toString().equals("a3ce929d0e0e4736")) //expect traceid from b3 not from x-b3-traceid) + assert(tid.spanId.toString().equals("00f067aa0ba902b7")) // expect spanid from b3 not from x-b3-spanid + assert(tid.sampled.contains(true)) // expect samplef from b3 not from x-b3-sampled + }} + } + + test("same trace from b3 single headers and x-b3- multi headers, 128bit traceid, UPPER CASE header don't matter") { + /* Turn on tracing and see if + b3=80f198ee56343ba864fe8b2a57d3eff7-05e3ac9a4f6e3b90-1-e457b5a2e4d86bd1 results in the same context as: + X-B3-TraceId: 80f198ee56343ba864fe8b2a57d3eff7 + X-B3-ParentSpanId: 05e3ac9a4f6e3b90 + X-B3-SpanId: e457b5a2e4d86bd1 + X-B3-Sampled: 1 + */ + + //NOTE: This will also test case doesn't matter for b3 single or x-b3- multi headers + val ztp = new ZipkinTracePropagator() + val req1 = Request() + req1.headerMap.add("B3", "80f198ee56343ba864fe8b2a57d3eff7-05e3ac9a4f6e3b90-1-e457b5a2e4d86bd1") + + val req2 = Request() + req2.headerMap.add("X-B3-TRACEID", "80f198ee56343ba864fe8b2a57d3eff7") + req2.headerMap.add("X-B3-SPANID", "05e3ac9a4f6e3b90") + req2.headerMap.add("X-B3-SAMPLED", "1") + req2.headerMap.add("X-B3-PARENTSPANID", "e457b5a2e4d86bd1") + + val trace1 = ztp.traceId(req1) + val trace2 = ztp.traceId(req2) + + assert(trace1 == trace2) + } + + test("cannot get trace from invalid b3 single header, too many fields") { + val ztp = new ZipkinTracePropagator() + val req1 = Request() + req1.headerMap.add("B3", "80f198ee56343ba864fe8b2a57d3eff7-05e3ac9a4f6e3b90-1-e457b5a2e4d86bd1-extra1") + + assert(ztp.traceId(req1).isEmpty) + } }