aboutsummaryrefslogtreecommitdiff
path: root/kamon-core-tests
diff options
context:
space:
mode:
Diffstat (limited to 'kamon-core-tests')
-rw-r--r--kamon-core-tests/src/test/resources/reference.conf34
-rw-r--r--kamon-core-tests/src/test/scala/kamon/context/BinaryPropagationSpec.scala174
-rw-r--r--kamon-core-tests/src/test/scala/kamon/context/ContextCodecSpec.scala93
-rw-r--r--kamon-core-tests/src/test/scala/kamon/context/ContextSerializationSpec.scala50
-rw-r--r--kamon-core-tests/src/test/scala/kamon/context/HttpPropagationSpec.scala159
-rw-r--r--kamon-core-tests/src/test/scala/kamon/context/ThreadLocalStorageSpec.scala8
-rw-r--r--kamon-core-tests/src/test/scala/kamon/instrumentation/HttpServerInstrumentationSpec.scala316
-rw-r--r--kamon-core-tests/src/test/scala/kamon/trace/B3SpanCodecSpec.scala208
-rw-r--r--kamon-core-tests/src/test/scala/kamon/trace/B3SpanPropagationSpec.scala232
9 files changed, 919 insertions, 355 deletions
diff --git a/kamon-core-tests/src/test/resources/reference.conf b/kamon-core-tests/src/test/resources/reference.conf
index 0d7ae9e2..c092af08 100644
--- a/kamon-core-tests/src/test/resources/reference.conf
+++ b/kamon-core-tests/src/test/resources/reference.conf
@@ -2,4 +2,38 @@ kamon {
context.codecs.string-keys {
request-id = "X-Request-ID"
}
+}
+
+
+
+kamon {
+
+ trace {
+ sampler = always
+ }
+
+ propagation.http.default {
+ tags.mappings {
+ "correlation-id" = "x-correlation-id"
+ }
+ }
+
+ instrumentation {
+ http-server {
+ default {
+ tracing.preferred-trace-id-tag = "correlation-id"
+ tracing.tags.from-context.peer = span
+ }
+
+ no-span-metrics {
+ tracing.span-metrics = off
+ }
+
+ noop {
+ propagation.enabled = no
+ metrics.enabled = no
+ tracing.enabled = no
+ }
+ }
+ }
} \ No newline at end of file
diff --git a/kamon-core-tests/src/test/scala/kamon/context/BinaryPropagationSpec.scala b/kamon-core-tests/src/test/scala/kamon/context/BinaryPropagationSpec.scala
new file mode 100644
index 00000000..5681d300
--- /dev/null
+++ b/kamon-core-tests/src/test/scala/kamon/context/BinaryPropagationSpec.scala
@@ -0,0 +1,174 @@
+package kamon.context
+
+import java.io.ByteArrayOutputStream
+
+import com.typesafe.config.ConfigFactory
+import kamon.Kamon
+import kamon.context.BinaryPropagation.{ByteStreamReader, ByteStreamWriter}
+import kamon.context.Propagation.{EntryReader, EntryWriter}
+import org.scalatest.{Matchers, OptionValues, WordSpec}
+
+import scala.util.Random
+
+class BinaryPropagationSpec extends WordSpec with Matchers with OptionValues {
+
+ "The Binary Context Propagation" should {
+ "return an empty context if there is no data to read from" in {
+ val context = binaryPropagation.read(ByteStreamReader.of(Array.ofDim[Byte](0)))
+ context.isEmpty() shouldBe true
+ }
+
+ "not write any data to the medium if the context is empty" in {
+ val writer = inspectableByteStreamWriter()
+ binaryPropagation.write(Context.Empty, writer)
+ writer.size() shouldBe 0
+ }
+
+ "handle malformed data in when reading a context" in {
+ val randomBytes = Array.ofDim[Byte](42)
+ Random.nextBytes(randomBytes)
+
+ val context = binaryPropagation.read(ByteStreamReader.of(randomBytes))
+ context.isEmpty() shouldBe true
+ }
+
+ "handle read failures in an entry reader" in {
+ val context = Context.of(
+ BinaryPropagationSpec.StringKey, "string-value",
+ BinaryPropagationSpec.FailStringKey, "fail-read"
+ )
+ val writer = inspectableByteStreamWriter()
+ binaryPropagation.write(context, writer)
+
+ val rtContext = binaryPropagation.read(ByteStreamReader.of(writer.toByteArray))
+ rtContext.tags shouldBe empty
+ rtContext.get(BinaryPropagationSpec.StringKey) shouldBe "string-value"
+ rtContext.get(BinaryPropagationSpec.FailStringKey) shouldBe null
+ }
+
+ "handle write failures in an entry writer" in {
+ val context = Context.of(
+ BinaryPropagationSpec.StringKey, "string-value",
+ BinaryPropagationSpec.FailStringKey, "fail-write"
+ )
+ val writer = inspectableByteStreamWriter()
+ binaryPropagation.write(context, writer)
+
+ val rtContext = binaryPropagation.read(ByteStreamReader.of(writer.toByteArray))
+ rtContext.tags shouldBe empty
+ rtContext.get(BinaryPropagationSpec.StringKey) shouldBe "string-value"
+ rtContext.get(BinaryPropagationSpec.FailStringKey) shouldBe null
+ }
+
+ "handle write failures in an entry writer when the context is too big" in {
+ val context = Context.of(BinaryPropagationSpec.StringKey, "string-value" * 20)
+ val writer = inspectableByteStreamWriter()
+ binaryPropagation.write(context, writer)
+
+ val rtContext = binaryPropagation.read(ByteStreamReader.of(writer.toByteArray))
+ rtContext shouldBe empty
+ }
+
+ "round trip a Context that only has tags" in {
+ val context = Context.of(Map("hello" -> "world", "kamon" -> "rulez"))
+ val writer = inspectableByteStreamWriter()
+ binaryPropagation.write(context, writer)
+
+ val rtContext = binaryPropagation.read(ByteStreamReader.of(writer.toByteArray))
+ rtContext.entries shouldBe empty
+ rtContext.tags should contain theSameElementsAs (context.tags)
+ }
+
+ "round trip a Context that only has entries" in {
+ val context = Context.of(BinaryPropagationSpec.StringKey, "string-value", BinaryPropagationSpec.IntegerKey, 42)
+ val writer = inspectableByteStreamWriter()
+ binaryPropagation.write(context, writer)
+
+ val rtContext = binaryPropagation.read(ByteStreamReader.of(writer.toByteArray))
+ rtContext.tags shouldBe empty
+ rtContext.get(BinaryPropagationSpec.StringKey) shouldBe "string-value"
+ rtContext.get(BinaryPropagationSpec.IntegerKey) shouldBe 0 // there is no entry configuration for the integer key
+ }
+
+ "round trip a Context that with tags and entries" in {
+ val context = Context.of(Map("hello" -> "world", "kamon" -> "rulez"))
+ .withKey(BinaryPropagationSpec.StringKey, "string-value")
+ .withKey(BinaryPropagationSpec.IntegerKey, 42)
+
+ val writer = inspectableByteStreamWriter()
+ binaryPropagation.write(context, writer)
+ val rtContext = binaryPropagation.read(ByteStreamReader.of(writer.toByteArray))
+
+ rtContext.tags should contain theSameElementsAs (context.tags)
+ rtContext.get(BinaryPropagationSpec.StringKey) shouldBe "string-value"
+ rtContext.get(BinaryPropagationSpec.IntegerKey) shouldBe 0 // there is no entry configuration for the integer key
+ }
+ }
+
+ val binaryPropagation = BinaryPropagation.from(
+ ConfigFactory.parseString(
+ """
+ |max-outgoing-size = 64
+ |entries.incoming.string = "kamon.context.BinaryPropagationSpec$StringEntryCodec"
+ |entries.incoming.failString = "kamon.context.BinaryPropagationSpec$FailStringEntryCodec"
+ |entries.outgoing.string = "kamon.context.BinaryPropagationSpec$StringEntryCodec"
+ |entries.outgoing.failString = "kamon.context.BinaryPropagationSpec$FailStringEntryCodec"
+ |
+ """.stripMargin
+ ).withFallback(ConfigFactory.load().getConfig("kamon.propagation")), Kamon)
+
+
+ def inspectableByteStreamWriter() = new ByteArrayOutputStream(32) with ByteStreamWriter
+
+}
+
+object BinaryPropagationSpec {
+
+ val StringKey = Context.key[String]("string", null)
+ val FailStringKey = Context.key[String]("failString", null)
+ val IntegerKey = Context.key[Int]("integer", 0)
+
+ class StringEntryCodec extends EntryReader[ByteStreamReader] with EntryWriter[ByteStreamWriter] {
+
+ override def read(medium: ByteStreamReader, context: Context): Context = {
+ val valueData = medium.readAll()
+
+ if(valueData.length > 0) {
+ context.withKey(StringKey, new String(valueData))
+ } else context
+ }
+
+ override def write(context: Context, medium: ByteStreamWriter): Unit = {
+ val value = context.get(StringKey)
+ if(value != null) {
+ medium.write(value.getBytes)
+ }
+ }
+ }
+
+ class FailStringEntryCodec extends EntryReader[ByteStreamReader] with EntryWriter[ByteStreamWriter] {
+
+ override def read(medium: ByteStreamReader, context: Context): Context = {
+ val valueData = medium.readAll()
+
+ if(valueData.length > 0) {
+ val stringValue = new String(valueData)
+ if(stringValue == "fail-read") {
+ sys.error("The fail string entry reader has triggered")
+ }
+
+ context.withKey(FailStringKey, stringValue)
+ } else context
+ }
+
+ override def write(context: Context, medium: ByteStreamWriter): Unit = {
+ val value = context.get(FailStringKey)
+ if(value != null && value != "fail-write") {
+ medium.write(value.getBytes)
+ } else {
+ medium.write(42) // malformed data on purpose
+ sys.error("The fail string entry writer has triggered")
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/kamon-core-tests/src/test/scala/kamon/context/ContextCodecSpec.scala b/kamon-core-tests/src/test/scala/kamon/context/ContextCodecSpec.scala
deleted file mode 100644
index f7bd7e56..00000000
--- a/kamon-core-tests/src/test/scala/kamon/context/ContextCodecSpec.scala
+++ /dev/null
@@ -1,93 +0,0 @@
-/* =========================================================================================
- * Copyright © 2013-2017 the kamon project <http://kamon.io/>
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
- * either express or implied. See the License for the specific language governing permissions
- * and limitations under the License.
- * =========================================================================================
- */
-
-package kamon.context
-
-import kamon.Kamon
-import kamon.testkit.ContextTesting
-import org.scalatest.{Matchers, OptionValues, WordSpec}
-
-class ContextCodecSpec extends WordSpec with Matchers with ContextTesting with OptionValues {
- "the Context Codec" when {
- "encoding/decoding to HttpHeaders" should {
- "round trip a empty context" in {
- val textMap = ContextCodec.HttpHeaders.encode(Context.Empty)
- val decodedContext = ContextCodec.HttpHeaders.decode(textMap)
-
- decodedContext shouldBe Context.Empty
- }
-
- "round trip a context with only local keys" in {
- val localOnlyContext = Context.create(StringKey, Some("string-value"))
- val textMap = ContextCodec.HttpHeaders.encode(localOnlyContext)
- val decodedContext = ContextCodec.HttpHeaders.decode(textMap)
-
- decodedContext shouldBe Context.Empty
- }
-
- "round trip a context with local and broadcast keys" in {
- val initialContext = Context.create()
- .withKey(StringKey, Some("string-value"))
- .withKey(StringBroadcastKey, Some("this-should-be-round-tripped"))
-
- val textMap = ContextCodec.HttpHeaders.encode(initialContext)
- val decodedContext = ContextCodec.HttpHeaders.decode(textMap)
-
- decodedContext.get(StringKey) shouldBe empty
- decodedContext.get(StringBroadcastKey).value shouldBe "this-should-be-round-tripped"
- }
-
- "read string broadcast keys using the configured header name" in {
- val textMap = TextMap.Default()
- textMap.put("X-Request-ID", "123456")
- val decodedContext = ContextCodec.HttpHeaders.decode(textMap)
-
- decodedContext.get(Key.broadcastString("request-id")).value shouldBe "123456"
- }
- }
-
- "encoding/decoding to Binary" should {
- "round trip a empty context" in {
- val byteBuffer = ContextCodec.Binary.encode(Context.Empty)
-
- val decodedContext = ContextCodec.Binary.decode(byteBuffer)
-
- decodedContext shouldBe Context.Empty
- }
-
- "round trip a context with only local keys" in {
- val localOnlyContext = Context.create(StringKey, Some("string-value"))
- val byteBuffer = ContextCodec.Binary.encode(localOnlyContext)
- val decodedContext = ContextCodec.Binary.decode(byteBuffer)
-
- decodedContext shouldBe Context.Empty
- }
-
- "round trip a context with local and broadcast keys" in {
- val initialContext = Context.create()
- .withKey(StringKey, Some("string-value"))
- .withKey(StringBroadcastKey, Some("this-should-be-round-tripped"))
-
- val byteBuffer = ContextCodec.Binary.encode(initialContext)
- val decodedContext = ContextCodec.Binary.decode(byteBuffer)
-
- decodedContext.get(StringKey) shouldBe empty
- decodedContext.get(StringBroadcastKey).value shouldBe "this-should-be-round-tripped"
- }
- }
- }
-
- val ContextCodec = new Codecs(Kamon.config())
-} \ No newline at end of file
diff --git a/kamon-core-tests/src/test/scala/kamon/context/ContextSerializationSpec.scala b/kamon-core-tests/src/test/scala/kamon/context/ContextSerializationSpec.scala
deleted file mode 100644
index f7e7599f..00000000
--- a/kamon-core-tests/src/test/scala/kamon/context/ContextSerializationSpec.scala
+++ /dev/null
@@ -1,50 +0,0 @@
-/* =========================================================================================
- * Copyright © 2013-2017 the kamon project <http://kamon.io/>
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
- * either express or implied. See the License for the specific language governing permissions
- * and limitations under the License.
- * =========================================================================================
- */
-
-package kamon.context
-
-import java.io.{ByteArrayInputStream, ByteArrayOutputStream, ObjectInputStream, ObjectOutputStream}
-
-import kamon.Kamon
-import kamon.testkit.ContextTesting
-import org.scalatest.{Matchers, OptionValues, WordSpec}
-
-class ContextSerializationSpec extends WordSpec with Matchers with ContextTesting with OptionValues {
- "the Context is Serializable" should {
- "empty " in {
- val bos = new ByteArrayOutputStream()
- val oos = new ObjectOutputStream(bos)
- oos.writeObject(Context.Empty)
-
- val ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray))
- val ctx = ois.readObject().asInstanceOf[Context]
- ctx shouldBe Context.Empty
- }
-
- "full" in {
- val sCtx = Context(StringBroadcastKey, Some("disi"))
- val bos = new ByteArrayOutputStream()
- val oos = new ObjectOutputStream(bos)
- oos.writeObject(sCtx)
-
- val ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray))
- val rCtx = ois.readObject().asInstanceOf[Context]
- rCtx shouldBe sCtx
- }
-
- }
-
- val ContextCodec = new Codecs(Kamon.config())
-} \ No newline at end of file
diff --git a/kamon-core-tests/src/test/scala/kamon/context/HttpPropagationSpec.scala b/kamon-core-tests/src/test/scala/kamon/context/HttpPropagationSpec.scala
new file mode 100644
index 00000000..fcddfe24
--- /dev/null
+++ b/kamon-core-tests/src/test/scala/kamon/context/HttpPropagationSpec.scala
@@ -0,0 +1,159 @@
+package kamon.context
+
+import com.typesafe.config.ConfigFactory
+import kamon.Kamon
+import kamon.context.HttpPropagation.{HeaderReader, HeaderWriter}
+import kamon.context.Propagation.{EntryReader, EntryWriter}
+import org.scalatest.{Matchers, OptionValues, WordSpec}
+
+import scala.collection.mutable
+
+class HttpPropagationSpec extends WordSpec with Matchers with OptionValues {
+
+ "The HTTP Context Propagation" when {
+ "reading from incoming requests" should {
+ "return an empty context if there are no tags nor keys" in {
+ val context = httpPropagation.read(headerReaderFromMap(Map.empty))
+ context.isEmpty() shouldBe true
+ }
+
+ "read tags from an HTTP message when they are available" in {
+ val headers = Map(
+ "x-content-tags" -> "hello=world;correlation=1234",
+ "x-mapped-tag" -> "value"
+ )
+ val context = httpPropagation.read(headerReaderFromMap(headers))
+ context.tags should contain only(
+ "hello" -> "world",
+ "correlation" -> "1234",
+ "mappedTag" -> "value"
+ )
+ }
+
+ "handle errors when reading HTTP headers" in {
+ val headers = Map("fail" -> "")
+ val context = httpPropagation.read(headerReaderFromMap(headers))
+ context.tags shouldBe empty
+ context.entries shouldBe empty
+ }
+
+ "read context with entries and tags" in {
+ val headers = Map(
+ "x-content-tags" -> "hello=world;correlation=1234",
+ "string-header" -> "hey",
+ "integer-header" -> "123"
+ )
+
+ val context = httpPropagation.read(headerReaderFromMap(headers))
+ context.get(HttpPropagationSpec.StringKey) shouldBe "hey"
+ context.get(HttpPropagationSpec.IntegerKey) shouldBe 123
+ context.get(HttpPropagationSpec.OptionalKey) shouldBe empty
+ context.getTag("hello").value shouldBe "world"
+ context.getTag("correlation").value shouldBe "1234"
+ context.getTag("unknown") shouldBe empty
+ }
+ }
+
+
+ "writing to outgoing requests" should {
+ "not write anything if the context is empty" in {
+ val headers = mutable.Map.empty[String, String]
+ httpPropagation.write(Context.Empty, headerWriterFromMap(headers))
+ headers shouldBe empty
+ }
+
+ "write context tags when available" in {
+ val headers = mutable.Map.empty[String, String]
+ val context = Context.of(Map(
+ "hello" -> "world",
+ "mappedTag" -> "value"
+ ))
+
+ httpPropagation.write(context, headerWriterFromMap(headers))
+ headers should contain only(
+ "x-content-tags" -> "hello=world;",
+ "x-mapped-tag" -> "value"
+ )
+ }
+
+ "write context entries when available" in {
+ val headers = mutable.Map.empty[String, String]
+ val context = Context.of(
+ HttpPropagationSpec.StringKey, "out-we-go",
+ HttpPropagationSpec.IntegerKey, 42
+ )
+
+ httpPropagation.write(context, headerWriterFromMap(headers))
+ headers should contain only(
+ "string-header" -> "out-we-go"
+ )
+ }
+ }
+ }
+
+
+ val httpPropagation = HttpPropagation.from(
+ ConfigFactory.parseString(
+ """
+ |tags {
+ | header-name = "x-content-tags"
+ |
+ | mappings {
+ | mappedTag = "x-mapped-tag"
+ | }
+ |}
+ |
+ |entries.incoming.string = "kamon.context.HttpPropagationSpec$StringEntryCodec"
+ |entries.incoming.integer = "kamon.context.HttpPropagationSpec$IntegerEntryCodec"
+ |entries.outgoing.string = "kamon.context.HttpPropagationSpec$StringEntryCodec"
+ |
+ """.stripMargin
+ ).withFallback(ConfigFactory.load().getConfig("kamon.propagation")), Kamon)
+
+
+ def headerReaderFromMap(map: Map[String, String]): HttpPropagation.HeaderReader = new HttpPropagation.HeaderReader {
+ override def read(header: String): Option[String] = {
+ if(map.get("fail").nonEmpty)
+ sys.error("failing on purpose")
+
+ map.get(header)
+ }
+
+ override def readAll(): Map[String, String] = map
+ }
+
+ def headerWriterFromMap(map: mutable.Map[String, String]): HttpPropagation.HeaderWriter = new HttpPropagation.HeaderWriter {
+ override def write(header: String, value: String): Unit = map.put(header, value)
+ }
+}
+
+object HttpPropagationSpec {
+
+ val StringKey = Context.key[String]("string", null)
+ val IntegerKey = Context.key[Int]("integer", 0)
+ val OptionalKey = Context.key[Option[String]]("optional", None)
+
+
+ class StringEntryCodec extends EntryReader[HeaderReader] with EntryWriter[HeaderWriter] {
+ private val HeaderName = "string-header"
+
+ override def read(reader: HttpPropagation.HeaderReader, context: Context): Context = {
+ reader.read(HeaderName)
+ .map(v => context.withKey(StringKey, v))
+ .getOrElse(context)
+ }
+
+ override def write(context: Context, writer: HttpPropagation.HeaderWriter): Unit = {
+ Option(context.get(StringKey)).foreach(v => writer.write(HeaderName, v))
+ }
+ }
+
+ class IntegerEntryCodec extends EntryReader[HeaderReader] {
+ override def read(reader: HttpPropagation.HeaderReader, context: Context): Context = {
+ reader.read("integer-header")
+ .map(v => context.withKey(IntegerKey, v.toInt))
+ .getOrElse(context)
+
+ }
+ }
+} \ No newline at end of file
diff --git a/kamon-core-tests/src/test/scala/kamon/context/ThreadLocalStorageSpec.scala b/kamon-core-tests/src/test/scala/kamon/context/ThreadLocalStorageSpec.scala
index d2c4e57e..8b94ac0c 100644
--- a/kamon-core-tests/src/test/scala/kamon/context/ThreadLocalStorageSpec.scala
+++ b/kamon-core-tests/src/test/scala/kamon/context/ThreadLocalStorageSpec.scala
@@ -48,8 +48,8 @@ class ThreadLocalStorageSpec extends WordSpec with Matchers {
}
val TLS: Storage = new Storage.ThreadLocal
- val TestKey = Key.local("test-key", 42)
- val AnotherKey = Key.local("another-key", 99)
- val BroadcastKey = Key.broadcast("broadcast", "i travel around")
- val ScopeWithKey = Context.create().withKey(TestKey, 43)
+ val TestKey = Context.key("test-key", 42)
+ val AnotherKey = Context.key("another-key", 99)
+ val BroadcastKey = Context.key("broadcast", "i travel around")
+ val ScopeWithKey = Context.of(TestKey, 43)
}
diff --git a/kamon-core-tests/src/test/scala/kamon/instrumentation/HttpServerInstrumentationSpec.scala b/kamon-core-tests/src/test/scala/kamon/instrumentation/HttpServerInstrumentationSpec.scala
new file mode 100644
index 00000000..c3c5f131
--- /dev/null
+++ b/kamon-core-tests/src/test/scala/kamon/instrumentation/HttpServerInstrumentationSpec.scala
@@ -0,0 +1,316 @@
+package kamon.instrumentation
+
+import java.time.Duration
+
+import kamon.context.Context
+import kamon.metric.{Counter, Histogram, RangeSampler}
+import kamon.testkit.{MetricInspection, SpanInspection}
+import org.scalatest.concurrent.Eventually
+import org.scalatest.{Matchers, OptionValues, WordSpec}
+
+import scala.collection.mutable
+
+class HttpServerInstrumentationSpec extends WordSpec with Matchers with SpanInspection with OptionValues with MetricInspection with Eventually {
+
+ "the HTTP server instrumentation" when {
+ "configured for context propagation" should {
+ "read context entries and tags from the incoming request" in {
+ val handler = httpServer().receive(fakeRequest("http://localhost:8080/", "/", "GET", Map(
+ "context-tags" -> "tag=value;none=0011223344556677;",
+ "custom-trace-id" -> "0011223344556677"
+ )))
+
+ handler.context.tags should contain only(
+ "tag" -> "value",
+ "none" -> "0011223344556677"
+ )
+
+ handler.send(fakeResponse(200, mutable.Map.empty), Context.Empty)
+ handler.doneSending(0L)
+ }
+
+ "use the configured HTTP propagation channel" in {
+ val handler = httpServer().receive(fakeRequest("http://localhost:8080/", "/", "GET", Map(
+ "context-tags" -> "tag=value;none=0011223344556677;",
+ "custom-trace-id" -> "0011223344556677"
+ )))
+
+ handler.context.tags should contain only(
+ "tag" -> "value",
+ "none" -> "0011223344556677"
+ )
+
+ val span = inspect(handler.span)
+ span.context().traceID.string shouldNot be("0011223344556677")
+ span.tag("http.method").value shouldBe "GET"
+ span.tag("http.url").value shouldBe "http://localhost:8080/"
+
+ val responseHeaders = mutable.Map.empty[String, String]
+ handler.send(fakeResponse(200, responseHeaders), handler.context.withTag("hello", "world"))
+ handler.doneSending(0L)
+ }
+ }
+
+ "configured for HTTP server metrics" should {
+ "track the number of open connections" in {
+ openConnections(8081).distribution()
+
+ httpServer().openConnection()
+ httpServer().openConnection()
+
+ val snapshotWithOpenConnections = openConnections(8081).distribution()
+ snapshotWithOpenConnections.min shouldBe 0
+ snapshotWithOpenConnections.max shouldBe 2
+
+ httpServer().closeConnection(Duration.ofSeconds(20), 10)
+ httpServer().closeConnection(Duration.ofSeconds(30), 15)
+
+ eventually {
+ val snapshotWithoutOpenConnections = openConnections(8081).distribution()
+ snapshotWithoutOpenConnections.min shouldBe 0
+ snapshotWithoutOpenConnections.max shouldBe 0
+ }
+ }
+
+ "track the distribution of number of requests handled per each connection" in {
+ connectionUsage(8081).distribution()
+
+ httpServer().openConnection()
+ httpServer().openConnection()
+ httpServer().closeConnection(Duration.ofSeconds(20), 10)
+ httpServer().closeConnection(Duration.ofSeconds(30), 15)
+
+ val connectionUsageSnapshot = connectionUsage(8081).distribution()
+ connectionUsageSnapshot.buckets.map(_.value) should contain allOf(
+ 10,
+ 15
+ )
+ }
+
+ "track the distribution of connection lifetime across all connections" in {
+ connectionLifetime(8081).distribution()
+
+ httpServer().openConnection()
+ httpServer().openConnection()
+ httpServer().closeConnection(Duration.ofSeconds(20), 10)
+ httpServer().closeConnection(Duration.ofSeconds(30), 15)
+
+ val connectionLifetimeSnapshot = connectionLifetime(8081).distribution()
+ connectionLifetimeSnapshot.buckets.map(_.value) should contain allOf(
+ 19998441472L, // 20 seconds with 1% precision
+ 29930553344L // 30 seconds with 1% precision
+ )
+ }
+
+ "track the number of active requests" in {
+ activeRequests(8081).distribution()
+
+ val handlerOne = httpServer().receive(fakeRequest("http://localhost:8080/", "/", "GET", Map.empty))
+ val handlerTwo = httpServer().receive(fakeRequest("http://localhost:8080/", "/", "GET", Map.empty))
+
+ val snapshotWithActiveRequests = activeRequests(8081).distribution()
+ snapshotWithActiveRequests.min shouldBe 0
+ snapshotWithActiveRequests.max shouldBe 2
+
+ handlerOne.send(fakeResponse(200, mutable.Map.empty), Context.Empty)
+ handlerTwo.send(fakeResponse(200, mutable.Map.empty), Context.Empty)
+ handlerOne.doneSending(0L)
+ handlerTwo.doneSending(0L)
+
+ eventually {
+ val snapshotWithoutActiveRequests = activeRequests(8081).distribution()
+ snapshotWithoutActiveRequests.min shouldBe 0
+ snapshotWithoutActiveRequests.max shouldBe 0
+ }
+ }
+
+ "track the distribution of sizes on incoming requests" in {
+ requestSize(8081).distribution()
+
+ httpServer().receive(fakeRequest("http://localhost:8080/", "/", "GET", Map.empty)).doneReceiving(300)
+ httpServer().receive(fakeRequest("http://localhost:8080/", "/", "GET", Map.empty)).doneReceiving(400)
+
+ val requestSizeSnapshot = requestSize(8081).distribution()
+ requestSizeSnapshot.buckets.map(_.value) should contain allOf(
+ 300,
+ 400
+ )
+ }
+
+ "track the distribution of sizes on outgoing responses" in {
+ responseSize(8081).distribution()
+
+ httpServer().receive(fakeRequest("http://localhost:8080/", "/", "GET", Map.empty)).doneSending(300)
+ httpServer().receive(fakeRequest("http://localhost:8080/", "/", "GET", Map.empty)).doneSending(400)
+
+ val requestSizeSnapshot = responseSize(8081).distribution()
+ requestSizeSnapshot.buckets.map(_.value) should contain allOf(
+ 300,
+ 400
+ )
+ }
+
+ "track the number of responses per status code" in {
+ // resets all counters
+ completedRequests(8081, 100).value()
+ completedRequests(8081, 200).value()
+ completedRequests(8081, 300).value()
+ completedRequests(8081, 400).value()
+ completedRequests(8081, 500).value()
+
+
+ val request = fakeRequest("http://localhost:8080/", "/", "GET", Map.empty)
+ httpServer().receive(request).send(fakeResponse(200, mutable.Map.empty), Context.Empty)
+ httpServer().receive(request).send(fakeResponse(302, mutable.Map.empty), Context.Empty)
+ httpServer().receive(request).send(fakeResponse(404, mutable.Map.empty), Context.Empty)
+ httpServer().receive(request).send(fakeResponse(504, mutable.Map.empty), Context.Empty)
+ httpServer().receive(request).send(fakeResponse(110, mutable.Map.empty), Context.Empty)
+
+ completedRequests(8081, 100).value() shouldBe 1L
+ completedRequests(8081, 200).value() shouldBe 1L
+ completedRequests(8081, 300).value() shouldBe 1L
+ completedRequests(8081, 400).value() shouldBe 1L
+ completedRequests(8081, 500).value() shouldBe 1L
+ }
+ }
+
+ "configured for distributed tracing" should {
+ "create a span representing the current HTTP operation" in {
+ val handler = httpServer().receive(fakeRequest("http://localhost:8080/", "/", "GET", Map.empty))
+ handler.send(fakeResponse(200, mutable.Map.empty), handler.context)
+
+ val span = inspect(handler.span)
+ span.tag("http.method").value shouldBe "GET"
+ span.tag("http.url").value shouldBe "http://localhost:8080/"
+ span.tag("http.status_code").value shouldBe "200"
+ }
+
+ "adopt a traceID when explicitly provided" in {
+ val handler = httpServer().receive(fakeRequest("http://localhost:8080/", "/", "GET", Map(
+ "context-tags" -> "tag=value;none=0011223344556677;",
+ "x-correlation-id" -> "0011223344556677"
+ )))
+
+ handler.span.context().traceID.string shouldBe "0011223344556677"
+ }
+
+ "record span metrics when enabled" in {
+ val handler = httpServer().receive(fakeRequest("http://localhost:8080/", "/", "GET", Map.empty))
+ handler.send(fakeResponse(200, mutable.Map.empty), handler.context)
+
+ val span = inspect(handler.span)
+ span.hasMetricsEnabled() shouldBe true
+ }
+
+ "not record span metrics when disabled" in {
+ val handler = noSpanMetricsHttpServer()
+ .receive(fakeRequest("http://localhost:8080/", "/", "GET", Map.empty))
+ handler.send(fakeResponse(200, mutable.Map.empty), handler.context)
+
+ val span = inspect(handler.span)
+ span.hasMetricsEnabled() shouldBe false
+ }
+
+ "receive tags from context when available" in {
+ val handler = httpServer().receive(fakeRequest("http://localhost:8080/", "/", "GET", Map(
+ "context-tags" -> "tag=value;none=0011223344556677;peer=superservice;",
+ "custom-trace-id" -> "0011223344556677"
+ )))
+
+ val span = inspect(handler.span)
+ span.tag("peer").value shouldBe "superservice"
+ }
+ }
+
+ "all capabilities are disabled" should {
+ "not read any context from the incoming requests" in {
+ val httpServer = noopHttpServer()
+ val handler = httpServer.receive(fakeRequest("http://localhost:8080/", "/", "GET", Map(
+ "context-tags" -> "tag=value;none=0011223344556677;",
+ "custom-trace-id" -> "0011223344556677"
+ )))
+
+ handler.context shouldBe Context.Empty
+ }
+
+ "not create any span to represent the server request" in {
+ val httpServer = noopHttpServer()
+ val handler = httpServer.receive(fakeRequest("http://localhost:8080/", "/", "GET", Map(
+ "context-tags" -> "tag=value;none=0011223344556677;",
+ "custom-trace-id" -> "0011223344556677"
+ )))
+
+ handler.span.isEmpty() shouldBe true
+ }
+
+ "not record any HTTP server metrics" in {
+ val request = fakeRequest("http://localhost:8080/", "/", "GET", Map.empty)
+ noopHttpServer().receive(request).send(fakeResponse(200, mutable.Map.empty), Context.Empty)
+ noopHttpServer().receive(request).send(fakeResponse(302, mutable.Map.empty), Context.Empty)
+ noopHttpServer().receive(request).send(fakeResponse(404, mutable.Map.empty), Context.Empty)
+ noopHttpServer().receive(request).send(fakeResponse(504, mutable.Map.empty), Context.Empty)
+ noopHttpServer().receive(request).send(fakeResponse(110, mutable.Map.empty), Context.Empty)
+
+ completedRequests(8083, 100).value() shouldBe 0L
+ completedRequests(8083, 200).value() shouldBe 0L
+ completedRequests(8083, 300).value() shouldBe 0L
+ completedRequests(8083, 400).value() shouldBe 0L
+ completedRequests(8083, 500).value() shouldBe 0L
+ }
+ }
+ }
+
+ val TestComponent = "http.server"
+ val TestInterface = "0.0.0.0"
+
+ def httpServer(): HttpServer = HttpServer.from("default", component = TestComponent, interface = TestInterface, port = 8081)
+ def noSpanMetricsHttpServer(): HttpServer = HttpServer.from("no-span-metrics", component = TestComponent, interface = TestInterface, port = 8082)
+ def noopHttpServer(): HttpServer = HttpServer.from("noop", component = TestComponent, interface = TestInterface, port = 8083)
+
+ def fakeRequest(requestUrl: String, requestPath: String, requestMethod: String, headers: Map[String, String]): HttpMessage.Request =
+ new HttpMessage.Request {
+ override def url: String = requestUrl
+ override def path: String = requestPath
+ override def method: String = requestMethod
+ override def read(header: String): Option[String] = headers.get(header)
+ override def readAll(): Map[String, String] = headers
+ }
+
+ def fakeResponse(responseStatusCode: Int, headers: mutable.Map[String, String]): HttpMessage.ResponseBuilder[HttpMessage.Response] =
+ new HttpMessage.ResponseBuilder[HttpMessage.Response] {
+ override def statusCode: Int = responseStatusCode
+ override def write(header: String, value: String): Unit = headers.put(header, value)
+ override def build(): HttpMessage.Response = this
+ }
+
+ def completedRequests(port: Int, statusCode: Int): Counter = {
+ val metrics = HttpServer.Metrics.of(TestComponent, TestInterface, port)
+
+ statusCode match {
+ case sc if sc >= 100 && sc <= 199 => metrics.requestsInformational
+ case sc if sc >= 200 && sc <= 299 => metrics.requestsSuccessful
+ case sc if sc >= 300 && sc <= 399 => metrics.requestsRedirection
+ case sc if sc >= 400 && sc <= 499 => metrics.requestsClientError
+ case sc if sc >= 500 && sc <= 599 => metrics.requestsServerError
+ }
+ }
+
+ def openConnections(port: Int): RangeSampler =
+ HttpServer.Metrics.of(TestComponent, TestInterface, port).openConnections
+
+ def connectionUsage(port: Int): Histogram =
+ HttpServer.Metrics.of(TestComponent, TestInterface, port).connectionUsage
+
+ def connectionLifetime(port: Int): Histogram =
+ HttpServer.Metrics.of(TestComponent, TestInterface, port).connectionLifetime
+
+ def activeRequests(port: Int): RangeSampler =
+ HttpServer.Metrics.of(TestComponent, TestInterface, port).activeRequests
+
+ def requestSize(port: Int): Histogram =
+ HttpServer.Metrics.of(TestComponent, TestInterface, port).requestSize
+
+ def responseSize(port: Int): Histogram =
+ HttpServer.Metrics.of(TestComponent, TestInterface, port).responseSize
+
+}
diff --git a/kamon-core-tests/src/test/scala/kamon/trace/B3SpanCodecSpec.scala b/kamon-core-tests/src/test/scala/kamon/trace/B3SpanCodecSpec.scala
deleted file mode 100644
index f718d806..00000000
--- a/kamon-core-tests/src/test/scala/kamon/trace/B3SpanCodecSpec.scala
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * =========================================================================================
- * Copyright © 2013-2017 the kamon project <http://kamon.io/>
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
- * either express or implied. See the License for the specific language governing permissions
- * and limitations under the License.
- * =========================================================================================
- */
-
-package kamon.trace
-
-import kamon.context.{Context, TextMap}
-import kamon.testkit.SpanBuilding
-import kamon.trace.IdentityProvider.Identifier
-import kamon.trace.SpanContext.SamplingDecision
-import org.scalatest.{Matchers, OptionValues, WordSpecLike}
-
-
-class B3SpanCodecSpec extends WordSpecLike with Matchers with OptionValues with SpanBuilding {
- val extendedB3Codec = SpanCodec.B3()
-
- "The ExtendedB3 SpanContextCodec" should {
- "return a TextMap containing the SpanContext data" in {
- val textMap = extendedB3Codec.encode(testContext())
- textMap.get("X-B3-TraceId").value shouldBe "1234"
- textMap.get("X-B3-ParentSpanId").value shouldBe "2222"
- textMap.get("X-B3-SpanId").value shouldBe "4321"
- textMap.get("X-B3-Sampled").value shouldBe "1"
- }
-
- "do not include the X-B3-ParentSpanId if there is no parent" in {
- val textMap = extendedB3Codec.encode(testContextWithoutParent())
- textMap.get("X-B3-TraceId").value shouldBe "1234"
- textMap.get("X-B3-ParentSpanId") shouldBe empty
- textMap.get("X-B3-SpanId").value shouldBe "4321"
- textMap.get("X-B3-Sampled").value shouldBe "1"
- }
-
-
- "not inject anything if there is no Span in the Context" in {
- val textMap = extendedB3Codec.encode(Context.Empty)
- textMap.values shouldBe empty
- }
-
- "extract a RemoteSpan from a TextMap when all fields are set" in {
- val textMap = TextMap.Default()
- textMap.put("X-B3-TraceId", "1234")
- textMap.put("X-B3-ParentSpanId", "2222")
- textMap.put("X-B3-SpanId", "4321")
- textMap.put("X-B3-Sampled", "1")
- textMap.put("X-B3-Extra-Baggage", "some=baggage;more=baggage")
-
- val spanContext = extendedB3Codec.decode(textMap, Context.Empty).get(Span.ContextKey).context()
- spanContext.traceID.string shouldBe "1234"
- spanContext.spanID.string shouldBe "4321"
- spanContext.parentID.string shouldBe "2222"
- spanContext.samplingDecision shouldBe SamplingDecision.Sample
- }
-
- "decode the sampling decision based on the X-B3-Sampled header" in {
- val sampledTextMap = TextMap.Default()
- sampledTextMap.put("X-B3-TraceId", "1234")
- sampledTextMap.put("X-B3-SpanId", "4321")
- sampledTextMap.put("X-B3-Sampled", "1")
-
- val notSampledTextMap = TextMap.Default()
- notSampledTextMap.put("X-B3-TraceId", "1234")
- notSampledTextMap.put("X-B3-SpanId", "4321")
- notSampledTextMap.put("X-B3-Sampled", "0")
-
- val noSamplingTextMap = TextMap.Default()
- noSamplingTextMap.put("X-B3-TraceId", "1234")
- noSamplingTextMap.put("X-B3-SpanId", "4321")
-
- extendedB3Codec.decode(sampledTextMap, Context.Empty)
- .get(Span.ContextKey).context().samplingDecision shouldBe SamplingDecision.Sample
-
- extendedB3Codec.decode(notSampledTextMap, Context.Empty)
- .get(Span.ContextKey).context().samplingDecision shouldBe SamplingDecision.DoNotSample
-
- extendedB3Codec.decode(noSamplingTextMap, Context.Empty)
- .get(Span.ContextKey).context().samplingDecision shouldBe SamplingDecision.Unknown
- }
-
- "not include the X-B3-Sampled header if the sampling decision is unknown" in {
- val context = testContext()
- val sampledSpanContext = context.get(Span.ContextKey).context()
- val notSampledSpanContext = Context.Empty.withKey(Span.ContextKey,
- Span.Remote(sampledSpanContext.copy(samplingDecision = SamplingDecision.DoNotSample)))
- val unknownSamplingSpanContext = Context.Empty.withKey(Span.ContextKey,
- Span.Remote(sampledSpanContext.copy(samplingDecision = SamplingDecision.Unknown)))
-
- extendedB3Codec.encode(context).get("X-B3-Sampled").value shouldBe("1")
- extendedB3Codec.encode(notSampledSpanContext).get("X-B3-Sampled").value shouldBe("0")
- extendedB3Codec.encode(unknownSamplingSpanContext).get("X-B3-Sampled") shouldBe empty
- }
-
- "use the Debug flag to override the sampling decision, if provided." in {
- val textMap = TextMap.Default()
- textMap.put("X-B3-TraceId", "1234")
- textMap.put("X-B3-SpanId", "4321")
- textMap.put("X-B3-Sampled", "0")
- textMap.put("X-B3-Flags", "1")
-
- val spanContext = extendedB3Codec.decode(textMap, Context.Empty).get(Span.ContextKey).context()
- spanContext.samplingDecision shouldBe SamplingDecision.Sample
- }
-
- "use the Debug flag as sampling decision when Sampled is not provided" in {
- val textMap = TextMap.Default()
- textMap.put("X-B3-TraceId", "1234")
- textMap.put("X-B3-SpanId", "4321")
- textMap.put("X-B3-Flags", "1")
-
- val spanContext = extendedB3Codec.decode(textMap, Context.Empty).get(Span.ContextKey).context()
- spanContext.samplingDecision shouldBe SamplingDecision.Sample
- }
-
- "extract a minimal SpanContext from a TextMap containing only the Trace ID and Span ID" in {
- val textMap = TextMap.Default()
- textMap.put("X-B3-TraceId", "1234")
- textMap.put("X-B3-SpanId", "4321")
-
- val spanContext = extendedB3Codec.decode(textMap, Context.Empty).get(Span.ContextKey).context()
- spanContext.traceID.string shouldBe "1234"
- spanContext.spanID.string shouldBe "4321"
- spanContext.parentID shouldBe IdentityProvider.NoIdentifier
- spanContext.samplingDecision shouldBe SamplingDecision.Unknown
- }
-
- "do not extract a SpanContext if Trace ID and Span ID are not provided" in {
- val onlyTraceID = TextMap.Default()
- onlyTraceID.put("X-B3-TraceId", "1234")
- onlyTraceID.put("X-B3-Sampled", "0")
- onlyTraceID.put("X-B3-Flags", "1")
-
- val onlySpanID = TextMap.Default()
- onlySpanID.put("X-B3-SpanId", "4321")
- onlySpanID.put("X-B3-Sampled", "0")
- onlySpanID.put("X-B3-Flags", "1")
-
- val noIds = TextMap.Default()
- noIds.put("X-B3-Sampled", "0")
- noIds.put("X-B3-Flags", "1")
-
- extendedB3Codec.decode(onlyTraceID, Context.Empty).get(Span.ContextKey) shouldBe Span.Empty
- extendedB3Codec.decode(onlySpanID, Context.Empty).get(Span.ContextKey) shouldBe Span.Empty
- extendedB3Codec.decode(noIds, Context.Empty).get(Span.ContextKey) shouldBe Span.Empty
- }
-
- "round trip a Span from TextMap -> Context -> TextMap" in {
- val textMap = TextMap.Default()
- textMap.put("X-B3-TraceId", "1234")
- textMap.put("X-B3-ParentSpanId", "2222")
- textMap.put("X-B3-SpanId", "4321")
- textMap.put("X-B3-Sampled", "1")
-
- val context = extendedB3Codec.decode(textMap, Context.Empty)
- val injectTextMap = extendedB3Codec.encode(context)
-
- textMap.values.toSeq should contain theSameElementsAs(injectTextMap.values.toSeq)
- }
-
- /*
- // TODO: Should we be supporting this use case? maybe even have the concept of Debug requests ourselves?
- "internally carry the X-B3-Flags value so that it can be injected in outgoing requests" in {
- val textMap = TextMap.Default()
- textMap.put("X-B3-TraceId", "1234")
- textMap.put("X-B3-ParentSpanId", "2222")
- textMap.put("X-B3-SpanId", "4321")
- textMap.put("X-B3-Sampled", "1")
- textMap.put("X-B3-Flags", "1")
-
- val spanContext = extendedB3Codec.extract(textMap).value
- val injectTextMap = extendedB3Codec.inject(spanContext)
-
- injectTextMap.get("X-B3-Flags").value shouldBe("1")
- }*/
- }
-
- def testContext(): Context = {
- val spanContext = createSpanContext().copy(
- traceID = Identifier("1234", Array[Byte](1, 2, 3, 4)),
- spanID = Identifier("4321", Array[Byte](4, 3, 2, 1)),
- parentID = Identifier("2222", Array[Byte](2, 2, 2, 2))
- )
-
- Context.create().withKey(Span.ContextKey, Span.Remote(spanContext))
- }
-
- def testContextWithoutParent(): Context = {
- val spanContext = createSpanContext().copy(
- traceID = Identifier("1234", Array[Byte](1, 2, 3, 4)),
- spanID = Identifier("4321", Array[Byte](4, 3, 2, 1)),
- parentID = IdentityProvider.NoIdentifier
- )
-
- Context.create().withKey(Span.ContextKey, Span.Remote(spanContext))
- }
-
-} \ No newline at end of file
diff --git a/kamon-core-tests/src/test/scala/kamon/trace/B3SpanPropagationSpec.scala b/kamon-core-tests/src/test/scala/kamon/trace/B3SpanPropagationSpec.scala
new file mode 100644
index 00000000..5b912b92
--- /dev/null
+++ b/kamon-core-tests/src/test/scala/kamon/trace/B3SpanPropagationSpec.scala
@@ -0,0 +1,232 @@
+/*
+ * =========================================================================================
+ * Copyright © 2013-2017 the kamon project <http://kamon.io/>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ * =========================================================================================
+ */
+
+package kamon.trace
+
+import kamon.context.{Context, HttpPropagation}
+import kamon.testkit.SpanBuilding
+import kamon.trace.IdentityProvider.Identifier
+import kamon.trace.SpanContext.SamplingDecision
+import org.scalatest.{Matchers, OptionValues, WordSpecLike}
+
+import scala.collection.mutable
+
+
+class B3SpanPropagationSpec extends WordSpecLike with Matchers with OptionValues with SpanBuilding {
+ val b3Propagation = SpanPropagation.B3()
+
+ "The B3 Span propagation for HTTP" should {
+ "write the Span data into headers" in {
+ val headersMap = mutable.Map.empty[String, String]
+ b3Propagation.write(testContext(), headerWriterFromMap(headersMap))
+
+ headersMap.get("X-B3-TraceId").value shouldBe "1234"
+ headersMap.get("X-B3-ParentSpanId").value shouldBe "2222"
+ headersMap.get("X-B3-SpanId").value shouldBe "4321"
+ headersMap.get("X-B3-Sampled").value shouldBe "1"
+ }
+
+ "do not include the X-B3-ParentSpanId if there is no parent" in {
+ val headersMap = mutable.Map.empty[String, String]
+ b3Propagation.write(testContextWithoutParent(), headerWriterFromMap(headersMap))
+
+ headersMap.get("X-B3-TraceId").value shouldBe "1234"
+ headersMap.get("X-B3-ParentSpanId") shouldBe empty
+ headersMap.get("X-B3-SpanId").value shouldBe "4321"
+ headersMap.get("X-B3-Sampled").value shouldBe "1"
+ }
+
+ "not inject anything if there is no Span in the Context" in {
+ val headersMap = mutable.Map.empty[String, String]
+ b3Propagation.write(Context.Empty, headerWriterFromMap(headersMap))
+ headersMap.values shouldBe empty
+ }
+
+ "extract a RemoteSpan from incoming headers when all fields are set" in {
+ val headersMap = Map(
+ "X-B3-TraceId" -> "1234",
+ "X-B3-ParentSpanId" -> "2222",
+ "X-B3-SpanId" -> "4321",
+ "X-B3-Sampled" -> "1",
+ "X-B3-Extra-Baggage" -> "some=baggage;more=baggage"
+ )
+
+ val spanContext = b3Propagation.read(headerReaderFromMap(headersMap), Context.Empty).get(Span.ContextKey).context()
+ spanContext.traceID.string shouldBe "1234"
+ spanContext.spanID.string shouldBe "4321"
+ spanContext.parentID.string shouldBe "2222"
+ spanContext.samplingDecision shouldBe SamplingDecision.Sample
+ }
+
+ "decode the sampling decision based on the X-B3-Sampled header" in {
+ val sampledHeaders = Map(
+ "X-B3-TraceId" -> "1234",
+ "X-B3-SpanId" -> "4321",
+ "X-B3-Sampled" -> "1"
+ )
+
+ val notSampledHeaders = Map(
+ "X-B3-TraceId" -> "1234",
+ "X-B3-SpanId" -> "4321",
+ "X-B3-Sampled" -> "0"
+ )
+
+ val noSamplingHeaders = Map(
+ "X-B3-TraceId" -> "1234",
+ "X-B3-SpanId" -> "4321"
+ )
+
+ b3Propagation.read(headerReaderFromMap(sampledHeaders), Context.Empty)
+ .get(Span.ContextKey).context().samplingDecision shouldBe SamplingDecision.Sample
+
+ b3Propagation.read(headerReaderFromMap(notSampledHeaders), Context.Empty)
+ .get(Span.ContextKey).context().samplingDecision shouldBe SamplingDecision.DoNotSample
+
+ b3Propagation.read(headerReaderFromMap(noSamplingHeaders), Context.Empty)
+ .get(Span.ContextKey).context().samplingDecision shouldBe SamplingDecision.Unknown
+ }
+
+ "not include the X-B3-Sampled header if the sampling decision is unknown" in {
+ val context = testContext()
+ val sampledSpanContext = context.get(Span.ContextKey).context()
+ val notSampledSpanContext = Context.Empty.withKey(Span.ContextKey,
+ Span.Remote(sampledSpanContext.copy(samplingDecision = SamplingDecision.DoNotSample)))
+ val unknownSamplingSpanContext = Context.Empty.withKey(Span.ContextKey,
+ Span.Remote(sampledSpanContext.copy(samplingDecision = SamplingDecision.Unknown)))
+ val headersMap = mutable.Map.empty[String, String]
+
+ b3Propagation.write(context, headerWriterFromMap(headersMap))
+ headersMap.get("X-B3-Sampled").value shouldBe("1")
+ headersMap.clear()
+
+ b3Propagation.write(notSampledSpanContext, headerWriterFromMap(headersMap))
+ headersMap.get("X-B3-Sampled").value shouldBe("0")
+ headersMap.clear()
+
+ b3Propagation.write(unknownSamplingSpanContext, headerWriterFromMap(headersMap))
+ headersMap.get("X-B3-Sampled") shouldBe empty
+ }
+
+ "use the Debug flag to override the sampling decision, if provided." in {
+ val headers = Map(
+ "X-B3-TraceId" -> "1234",
+ "X-B3-SpanId" -> "4321",
+ "X-B3-Sampled" -> "0",
+ "X-B3-Flags" -> "1"
+ )
+
+ val spanContext = b3Propagation.read(headerReaderFromMap(headers), Context.Empty).get(Span.ContextKey).context()
+ spanContext.samplingDecision shouldBe SamplingDecision.Sample
+ }
+
+ "use the Debug flag as sampling decision when Sampled is not provided" in {
+ val headers = Map(
+ "X-B3-TraceId" -> "1234",
+ "X-B3-SpanId" -> "4321",
+ "X-B3-Flags" -> "1"
+ )
+
+ val spanContext = b3Propagation.read(headerReaderFromMap(headers), Context.Empty).get(Span.ContextKey).context()
+ spanContext.samplingDecision shouldBe SamplingDecision.Sample
+ }
+
+ "extract a minimal SpanContext from a TextMap containing only the Trace ID and Span ID" in {
+ val headers = Map(
+ "X-B3-TraceId" -> "1234",
+ "X-B3-SpanId" -> "4321"
+ )
+
+ val spanContext = b3Propagation.read(headerReaderFromMap(headers), Context.Empty).get(Span.ContextKey).context()
+ spanContext.traceID.string shouldBe "1234"
+ spanContext.spanID.string shouldBe "4321"
+ spanContext.parentID shouldBe IdentityProvider.NoIdentifier
+ spanContext.samplingDecision shouldBe SamplingDecision.Unknown
+ }
+
+ "do not extract a SpanContext if Trace ID and Span ID are not provided" in {
+ val onlyTraceID = Map(
+ "X-B3-TraceId" -> "1234",
+ "X-B3-Sampled" -> "0",
+ "X-B3-Flags" -> "1"
+ )
+
+ val onlySpanID = Map(
+ "X-B3-SpanId" -> "1234",
+ "X-B3-Sampled" -> "0",
+ "X-B3-Flags" -> "1"
+ )
+
+ val noIds = Map(
+ "X-B3-Sampled" -> "0",
+ "X-B3-Flags" -> "1"
+ )
+
+ b3Propagation.read(headerReaderFromMap(onlyTraceID), Context.Empty).get(Span.ContextKey) shouldBe Span.Empty
+ b3Propagation.read(headerReaderFromMap(onlySpanID), Context.Empty).get(Span.ContextKey) shouldBe Span.Empty
+ b3Propagation.read(headerReaderFromMap(noIds), Context.Empty).get(Span.ContextKey) shouldBe Span.Empty
+ }
+
+ "round trip a Span from TextMap -> Context -> TextMap" in {
+ val headers = Map(
+ "X-B3-TraceId" -> "1234",
+ "X-B3-SpanId" -> "4321",
+ "X-B3-ParentSpanId" -> "2222",
+ "X-B3-Sampled" -> "1"
+ )
+
+ val writenHeaders = mutable.Map.empty[String, String]
+ val context = b3Propagation.read(headerReaderFromMap(headers), Context.Empty)
+ b3Propagation.write(context, headerWriterFromMap(writenHeaders))
+ writenHeaders should contain theSameElementsAs(headers)
+ }
+ }
+
+ def headerReaderFromMap(map: Map[String, String]): HttpPropagation.HeaderReader = new HttpPropagation.HeaderReader {
+ override def read(header: String): Option[String] = {
+ if(map.get("fail").nonEmpty)
+ sys.error("failing on purpose")
+
+ map.get(header)
+ }
+
+ override def readAll(): Map[String, String] = map
+ }
+
+ def headerWriterFromMap(map: mutable.Map[String, String]): HttpPropagation.HeaderWriter = new HttpPropagation.HeaderWriter {
+ override def write(header: String, value: String): Unit = map.put(header, value)
+ }
+
+ def testContext(): Context = {
+ val spanContext = createSpanContext().copy(
+ traceID = Identifier("1234", Array[Byte](1, 2, 3, 4)),
+ spanID = Identifier("4321", Array[Byte](4, 3, 2, 1)),
+ parentID = Identifier("2222", Array[Byte](2, 2, 2, 2))
+ )
+
+ Context.of(Span.ContextKey, Span.Remote(spanContext))
+ }
+
+ def testContextWithoutParent(): Context = {
+ val spanContext = createSpanContext().copy(
+ traceID = Identifier("1234", Array[Byte](1, 2, 3, 4)),
+ spanID = Identifier("4321", Array[Byte](4, 3, 2, 1)),
+ parentID = IdentityProvider.NoIdentifier
+ )
+
+ Context.of(Span.ContextKey, Span.Remote(spanContext))
+ }
+
+} \ No newline at end of file