From c487c51a54e67944c80cf2aecc63ac8158bf99a6 Mon Sep 17 00:00:00 2001 From: Ivan Topolnjak Date: Mon, 3 Sep 2018 15:37:14 +0200 Subject: wip on the HttpServer instrumentation --- kamon-core-tests/src/test/resources/reference.conf | 119 +++++++++++++++++++++ .../scala/kamon/context/HttpPropagationSpec.scala | 30 +++--- .../HttpServerInstrumentationSpec.scala | 88 +++++++++++++++ 3 files changed, 222 insertions(+), 15 deletions(-) create mode 100644 kamon-core-tests/src/test/scala/kamon/instrumentation/HttpServerInstrumentationSpec.scala (limited to 'kamon-core-tests') diff --git a/kamon-core-tests/src/test/resources/reference.conf b/kamon-core-tests/src/test/resources/reference.conf index 0d7ae9e2..d165249e 100644 --- a/kamon-core-tests/src/test/resources/reference.conf +++ b/kamon-core-tests/src/test/resources/reference.conf @@ -2,4 +2,123 @@ kamon { context.codecs.string-keys { request-id = "X-Request-ID" } +} + + + +kamon { + instrumentation { + http-server { + noop { + + # + # Configuration for HTTP context propagation + # + propagation { + + # Enables or disables HTTP context propagation on this HTTP server instrumentation. Please note that if + # propagation is disabled then some distributed tracing features will not be work as expected (e.g. Spans can + # be created and reported but will not be linked across boundaries nor take trace identifiers from tags). + enabled = no + + # HTTP propagation channel to b used by this instrumentation. Take a look at the kamon.propagation.http.default + # configuration for more details on how to configure the detault HTTP context propagation. + channel = "default" + } + + + # + # Configuration for HTTP server metrics collection + # + metrics { + + # Enables collection of HTTP server metrics + enabled = no + + # Tags to include on the HTTP server metrics. The available options are: + # - method: HTTP method from the request. + # - status-code: HTTP status code from the responses. + # + tags = [ + "method", + "status-code" + ] + } + + + # + # Configuration for HTTP request tracing + # + tracing { + + # Enables HTTP request tracing. When enabled the instrumentation will create Spans for incoming requests + # and finish them when the response is sent back to the clients. + enabled = no + + # Select a context tag that provides a custom trace identifier. The custom trace identifier will be used + # only if all these conditions are met: + # - the context tag is present. + # - there is no parent Span on the incoming context (i.e. this is the first service on the trace). + # - the identifier is valued in accordance to the identity provider. + trace-id-tag = "none" + + # Enables collection of span metrics using the `span.processing-time` metric. + span-metrics = on + + # Select which tags should be included as span and span metric tags. The possible options are: + # - span: the tag is added as a Span tag (i.e. using span.tag(...)) + # - metric: the tag is added a a Span metric tag (i.e. using span.tagMetric(...)) + # - off: the tag is not used. + # + tags { + + # Use the http.url tag. + url = span + + # Use the http.method tag. + method = metric + + # Use the http.status_code tag. + status-code = metric + + # Copy tags from the context into the Spans with the specified purpouse. + from-context { + + # The peer tag identifiest the service that is calling the current service. It is added by default with + # the HttpClient instrumentation. + peer = metric + } + } + + # Custom mappings between routes and operation names. + operations { + + # Operation name for Spans created on requests that could not be handled by any route in the current + # application. + unhandled = "unhandled" + + # Provides custom mappings from HTTP paths into operation names. Meant to be used in cases where the bytecode + # instrumentation is not able to provide a sensible operation name that is free of high cardinality values. + # For example, with the following configuration: + # mappings { + # "/organization/*/user/*/profile" = "/organization/:orgID/user/:userID/profile" + # "/events/*/rsvps" = "EventRSVPs" + # } + # + # Requests to "/organization/3651/user/39652/profile" and "/organization/22234/user/54543/profile" will have + # the same operation name "/organization/:orgID/user/:userID/profile". + # + # Similarly, requests to "/events/aaa-bb-ccc/rsvps" and "/events/1234/rsvps" will have the same operation + # name "EventRSVPs". + # + # The patterns are expressed as globs and the operation names are free form. + # + mappings { + + } + } + } + } + } + } } \ 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 index 08d0b691..44165b98 100644 --- a/kamon-core-tests/src/test/scala/kamon/context/HttpPropagationSpec.scala +++ b/kamon-core-tests/src/test/scala/kamon/context/HttpPropagationSpec.scala @@ -12,7 +12,7 @@ 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)) + val context = httpPropagation.readContext(headerReaderFromMap(Map.empty)) context.tags shouldBe empty context.entries shouldBe empty } @@ -22,7 +22,7 @@ class HttpPropagationSpec extends WordSpec with Matchers with OptionValues { "x-content-tags" -> "hello=world;correlation=1234", "x-mapped-tag" -> "value" ) - val context = httpPropagation.read(headerReaderFromMap(headers)) + val context = httpPropagation.readContext(headerReaderFromMap(headers)) context.tags should contain only( "hello" -> "world", "correlation" -> "1234", @@ -32,7 +32,7 @@ class HttpPropagationSpec extends WordSpec with Matchers with OptionValues { "handle errors when reading HTTP headers" in { val headers = Map("fail" -> "") - val context = httpPropagation.read(headerReaderFromMap(headers)) + val context = httpPropagation.readContext(headerReaderFromMap(headers)) context.tags shouldBe empty context.entries shouldBe empty } @@ -44,7 +44,7 @@ class HttpPropagationSpec extends WordSpec with Matchers with OptionValues { "integer-header" -> "123" ) - val context = httpPropagation.read(headerReaderFromMap(headers)) + val context = httpPropagation.readContext(headerReaderFromMap(headers)) context.get(HttpPropagationSpec.StringKey) shouldBe "hey" context.get(HttpPropagationSpec.IntegerKey) shouldBe 123 context.get(HttpPropagationSpec.OptionalKey) shouldBe empty @@ -66,7 +66,7 @@ class HttpPropagationSpec extends WordSpec with Matchers with OptionValues { def propagationWritingTests(direction: Direction.Write) = { "not write anything if the context is empty" in { val headers = mutable.Map.empty[String, String] - httpPropagation.write(Context.Empty, headerWriterFromMap(headers), direction) + httpPropagation.writeContext(Context.Empty, headerWriterFromMap(headers), direction) headers shouldBe empty } @@ -77,7 +77,7 @@ class HttpPropagationSpec extends WordSpec with Matchers with OptionValues { "mappedTag" -> "value" )) - httpPropagation.write(context, headerWriterFromMap(headers), direction) + httpPropagation.writeContext(context, headerWriterFromMap(headers), direction) headers should contain only( "x-content-tags" -> "hello=world;", "x-mapped-tag" -> "value" @@ -91,7 +91,7 @@ class HttpPropagationSpec extends WordSpec with Matchers with OptionValues { HttpPropagationSpec.IntegerKey, 42, ) - httpPropagation.write(context, headerWriterFromMap(headers), direction) + httpPropagation.writeContext(context, headerWriterFromMap(headers), direction) headers should contain only( "string-header" -> "out-we-go" ) @@ -123,7 +123,7 @@ class HttpPropagationSpec extends WordSpec with Matchers with OptionValues { def headerReaderFromMap(map: Map[String, String]): HttpPropagation.HeaderReader = new HttpPropagation.HeaderReader { - override def read(header: String): Option[String] = { + override def readHeader(header: String): Option[String] = { if(map.get("fail").nonEmpty) sys.error("failing on purpose") @@ -132,7 +132,7 @@ class HttpPropagationSpec extends WordSpec with Matchers with OptionValues { } def headerWriterFromMap(map: mutable.Map[String, String]): HttpPropagation.HeaderWriter = new HttpPropagation.HeaderWriter { - override def write(header: String, value: String): Unit = map.put(header, value) + override def writeHeader(header: String, value: String): Unit = map.put(header, value) } } @@ -146,20 +146,20 @@ object HttpPropagationSpec { class StringEntryCodec extends HttpPropagation.EntryReader with HttpPropagation.EntryWriter { private val HeaderName = "string-header" - override def read(reader: HttpPropagation.HeaderReader, context: Context): Context = { - reader.read(HeaderName) + override def readEntry(reader: HttpPropagation.HeaderReader, context: Context): Context = { + reader.readHeader(HeaderName) .map(v => context.withKey(StringKey, v)) .getOrElse(context) } - override def write(context: Context, writer: HttpPropagation.HeaderWriter, direction: Direction.Write): Unit = { - Option(context.get(StringKey)).foreach(v => writer.write(HeaderName, v)) + override def writeEntry(context: Context, writer: HttpPropagation.HeaderWriter, direction: Direction.Write): Unit = { + Option(context.get(StringKey)).foreach(v => writer.writeHeader(HeaderName, v)) } } class IntegerEntryCodec extends HttpPropagation.EntryReader { - override def read(reader: HttpPropagation.HeaderReader, context: Context): Context = { - reader.read("integer-header") + override def readEntry(reader: HttpPropagation.HeaderReader, context: Context): Context = { + reader.readHeader("integer-header") .map(v => context.withKey(IntegerKey, v.toInt)) .getOrElse(context) 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..a3662127 --- /dev/null +++ b/kamon-core-tests/src/test/scala/kamon/instrumentation/HttpServerInstrumentationSpec.scala @@ -0,0 +1,88 @@ +package kamon.instrumentation + +import kamon.context.Context +import kamon.testkit.SpanInspection +import org.scalatest.{Matchers, OptionValues, WordSpec} + +import scala.collection.mutable + +class HttpServerInstrumentationSpec extends WordSpec with Matchers with SpanInspection with OptionValues { + + "the HTTP server instrumentation" when { + "configured for context propagation" should { + "read context entries and tags from the incoming request" in { + val httpServer = HttpServer.from("default", port = 8080, "http.server") + val handler = httpServer.handle(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" + ) + } + + "use the configured HTTP propagation channel" in { + val httpServer = HttpServer.from("default", port = 8080, "http.server") + val handler = httpServer.handle(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.startResponse(fakeResponse(200, responseHeaders), handler.context.withTag("hello", "world")) + + } + } + + "when all capabilities are disabled" should { + "not read any context from the incoming requests" in { + val httpServer = HttpServer.from("noop", port = 8081, "http.server") + val handler = httpServer.handle(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 = HttpServer.from("noop", port = 8081, "http.server") + val handler = httpServer.handle(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" is (pending) + } + } + + def fakeRequest(requestUrl: String, requestPath: String, requestMethod: String, headers: Map[String, String]): HttpRequest = + new HttpRequest { + override def url: String = requestUrl + override def path: String = requestPath + override def method: String = requestMethod + override def readHeader(header: String): Option[String] = headers.get(header) + } + + def fakeResponse(responseStatusCode: Int, headers: mutable.Map[String, String]): HttpResponse.Writable[HttpResponse] = + new HttpResponse.Writable[HttpResponse] { + override def statusCode: Int = responseStatusCode + override def writeHeader(header: String, value: String): Unit = headers.put(header, value) + override def build(): HttpResponse = this + } +} -- cgit v1.2.3