Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configurable operation name mappings for http client instrumentation #771

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package kamon.instrumentation.akka.http

import akka.http.scaladsl.model.{HttpHeader, HttpRequest, HttpResponse}
import akka.http.scaladsl.model.{HttpRequest, HttpResponse}
import akka.http.scaladsl.model.headers.RawHeader
import kamon.Kamon
import kamon.instrumentation.http.HttpMessage

import scala.collection.immutable

object AkkaHttpInstrumentation {

Kamon.onReconfigure(_ => AkkaHttpClientInstrumentation.rebuildHttpClientInstrumentation(): Unit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,14 @@ kamon.instrumentation.akka.http {
}
}
}

client {
tracing {
operations {
mappings {
"/name-will-be-changed" = "named-via-config"
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,16 @@ package kamon.akka.http

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.headers.RawHeader
import akka.http.scaladsl.model.HttpRequest
import akka.http.scaladsl.model.headers.RawHeader
import akka.stream.ActorMaterializer
import kamon.Kamon
import kamon.tag.Lookups.{plain, plainBoolean, plainLong}
import kamon.testkit._
import kamon.trace.Span
import kamon.tag.Lookups.{plain, plainLong, plainBoolean}
import org.scalatest.concurrent.Eventually
import org.scalatest.{BeforeAndAfterAll, Matchers, OptionValues, WordSpecLike}
import org.json4s._
import org.json4s.native.JsonMethods._
import org.scalatest.concurrent.Eventually
import org.scalatest.{BeforeAndAfterAll, Matchers, OptionValues, WordSpecLike}

import scala.concurrent.duration._

Expand Down Expand Up @@ -115,6 +114,16 @@ class AkkaHttpClientTracingSpec extends WordSpecLike with Matchers with BeforeAn
span.hasError shouldBe true
}
}

"keep operation names provided by the HTTP Client instrumentation" in {
val target = s"http://$interface:$port/name-will-be-changed"
Http().singleRequest(HttpRequest(uri = target)).map(_.discardEntityBytes())

eventually(timeout(10 seconds)) {
val span = testSpanReporter().nextSpan().value
span.operationName shouldBe "named-via-config"
}
}
}

override protected def afterAll(): Unit = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,16 @@ package kamon.akka.http
import java.util.UUID

import akka.actor.ActorSystem
import akka.http.scaladsl.UseHttp2
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.ActorMaterializer
import javax.net.ssl.{HostnameVerifier, SSLSession}
import kamon.testkit._
import kamon.tag.Lookups.{plain, plainBoolean, plainLong}
import kamon.testkit._
import kamon.trace.Span.Mark
import okhttp3.{OkHttpClient, Request}
import org.scalatest._
import org.scalatest.concurrent.{Eventually, ScalaFutures}

import scala.concurrent.duration._
import okhttp3.{OkHttpClient, Request}

class AkkaHttpServerTracingSpec extends WordSpecLike with Matchers with ScalaFutures with Inside with BeforeAndAfterAll
with MetricInspection.Syntax with Reconfigure with TestWebServer with Eventually with OptionValues with TestSpanReporter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,26 @@ kamon {
# - method: Uses the request HTTP method as the operation name.
#
name-generator = "method"

# 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"
# }
#
# Client 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 {

}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import kamon.instrumentation.trace.SpanTagger
import kamon.instrumentation.trace.SpanTagger.TagMode
import kamon.tag.Lookups.option
import kamon.trace.Span
import kamon.util.Filter
import org.slf4j.LoggerFactory

import scala.util.Try
Expand Down Expand Up @@ -131,7 +132,7 @@ object HttpClientInstrumentation {

private def createClientSpan(requestMessage: HttpMessage.Request, context: Context): Span = {
val span = Kamon
.clientSpanBuilder(operationName(requestMessage), component)
.clientSpanBuilder(settings.operationNameSettings.operationName(requestMessage), component)
.context(context)

if(!settings.enableSpanMetrics)
Expand All @@ -149,12 +150,9 @@ object HttpClientInstrumentation {

span.start()
}

private def operationName(requestMessage: HttpMessage.Request): String =
settings.operationNameGenerator.name(requestMessage).getOrElse(settings.defaultOperationName)
}

case class Settings (
final case class Settings (
enableContextPropagation: Boolean,
propagationChannel: String,
enableTracing: Boolean,
Expand All @@ -164,8 +162,11 @@ object HttpClientInstrumentation {
statusCodeTagMode: TagMode,
contextTags: Map[String, TagMode],
defaultOperationName: String,
operationMappings: Map[Filter.Glob, String],
operationNameGenerator: HttpOperationNameGenerator
)
) {
val operationNameSettings = OperationNameSettings(defaultOperationName, operationMappings, operationNameGenerator)
}

object Settings {

Expand Down Expand Up @@ -197,6 +198,9 @@ object HttpClientInstrumentation {
_log.warn("Failed to create an HTTP Operation Name Generator, falling back to the default operation name", t)
new HttpOperationNameGenerator.Static(defaultOperationName)
}
val operationMappings = config.getConfig("tracing.operations.mappings").pairs.map {
case (pattern, operationName) => (Filter.Glob(pattern), operationName)
}

Settings(
enablePropagation,
Expand All @@ -208,6 +212,7 @@ object HttpClientInstrumentation {
statusCodeTagMode,
contextTags,
defaultOperationName,
operationMappings,
operationNameGenerator.get
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ object HttpServerInstrumentation {


private def buildServerSpan(context: Context, request: HttpMessage.Request, deferSamplingDecision: Boolean): Span = {
val span = Kamon.serverSpanBuilder(operationName(request), component)
val span = Kamon.serverSpanBuilder(settings.operationNameSettings.operationName(request), component)
span.context(context)

if(!settings.enableSpanMetrics)
Expand All @@ -322,21 +322,10 @@ object HttpServerInstrumentation {

span.start()
}

private def operationName(request: HttpMessage.Request): String = {
val requestPath = request.path
//first apply any mappings rules
val customMapping = settings.operationMappings.collectFirst {
case (pattern, operationName) if pattern.accept(requestPath) => operationName
}.orElse( //fallback to use any configured name generator
settings.operationNameGenerator.name(request)
)
customMapping.getOrElse(settings.defaultOperationName)
}
}


case class Settings(
final case class Settings(
enableContextPropagation: Boolean,
propagationChannel: String,
enableServerMetrics: Boolean,
Expand All @@ -353,7 +342,9 @@ object HttpServerInstrumentation {
unhandledOperationName: String,
operationMappings: Map[Filter.Glob, String],
operationNameGenerator: HttpOperationNameGenerator
)
) {
val operationNameSettings = OperationNameSettings(defaultOperationName, operationMappings, operationNameGenerator)
}

object Settings {

Expand Down Expand Up @@ -395,7 +386,7 @@ object HttpServerInstrumentation {
}
val unhandledOperationName = config.getString("tracing.operations.unhandled")
val operationMappings = config.getConfig("tracing.operations.mappings").pairs.map {
case (pattern, operationName) => (new Filter.Glob(pattern), operationName)
case (pattern, operationName) => (Filter.Glob(pattern), operationName)
}

Settings(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package kamon.instrumentation.http

import kamon.util.Filter

final case class OperationNameSettings(defaultOperationName: String,
operationMappings: Map[Filter.Glob, String],
operationNameGenerator: HttpOperationNameGenerator) {
private[http] def operationName(request: HttpMessage.Request): String = {
val requestPath = request.path
//first apply any mappings rules
val customMapping = operationMappings.collectFirst {
case (pattern, operationName) if pattern.accept(requestPath) => operationName
}.orElse( //fallback to use any configured name generator
operationNameGenerator.name(request)
)
customMapping.getOrElse(defaultOperationName)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ kamon {
}

http-client {
default {
tracing {
operations {
default = "default-name"
mappings {
"/events/*/rsvps" = "EventRSVPs"
}
}
}
}

no-span-metrics {
tracing.span-metrics = off
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ class HttpClientInstrumentationSpec extends WordSpec with Matchers with Instrume
handler.request.read("X-B3-ParentSpanId").value shouldBe parent.id.string
handler.request.read("X-B3-Sampled") shouldBe defined
}

"honour user provided operation name mappings" in {
val handler = httpClient().createHandler(fakeRequest("http://localhost:8080/", "/events/123/rsvps", "GET", Map.empty), Context.Empty)
handler.processResponse(fakeResponse(200))

val span = handler.span
span.operationName() shouldBe "EventRSVPs"
span.tags().get(plain("http.method")) shouldBe "GET"
span.tags().get(plain("http.url")) shouldBe "http://localhost:8080/"
span.tags().get(plainLong("http.status_code")) shouldBe 200
span.tags().get(plain("operation")) shouldBe "EventRSVPs"
}
}

"all capabilities are disabled" should {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ class StatementInstrumentationSpec extends WordSpec with Matchers with Eventuall
statement.execute()
validateNextRow(statement.getResultSet, valueNr = 1, valueName = "foo")

eventually(timeout(scaled(5 seconds))) {
eventually(timeout(scaled(5 seconds)), interval(200 millis)) {
ivantopo marked this conversation as resolved.
Show resolved Hide resolved
val span = testSpanReporter().nextSpan().value
span.operationName shouldBe StatementTypes.GenericExecute
span.metricTags.get(plain("component")) shouldBe "jdbc"
Expand Down