diff options
Diffstat (limited to 'kamon-core-tests')
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 |