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)
}
}
}