package kamon package instrumentation import com.typesafe.config.Config import kamon.context.Context import kamon.context.HttpPropagation.Direction import kamon.instrumentation.HttpServer.Settings.TagMode import kamon.trace.Span import kamon.util.GlobPathFilter import scala.collection.JavaConverters._ trait HttpServer { def handle(request: HttpRequest): HttpServer.Handler } object HttpServer { trait Handler { def context: Context def span: Span def finishRequest(): Unit def startResponse[HttpResponse](response: HttpResponse.Writable[HttpResponse], context: Context): HttpResponse def endResponse(): Unit } def from(name: String, port: Int, component: String): HttpServer = { from(name, port, component, Kamon, Kamon) } def from(name: String, port: Int, component: String, configuration: Configuration, contextPropagation: ContextPropagation): HttpServer = { val configKey = "kamon.instrumentation.http-server." + name new HttpServer.Default(Settings.from(configuration.config().getConfig(configKey)), contextPropagation, port, component) } class Default(settings: Settings, contextPropagation: ContextPropagation, port: Int, component: String) extends HttpServer { private val _propagation = contextPropagation.httpPropagation(settings.propagationChannel) .getOrElse(sys.error(s"Could not find HTTP propagation [${settings.propagationChannel}")) override def handle(request: HttpRequest): Handler = { val incomingContext = if(settings.enableContextPropagation) _propagation.readContext(request) else Context.Empty val requestSpan = if(settings.enableTracing) buildServerSpan(incomingContext, request) else Span.Empty val handlerContext = if(requestSpan.nonEmpty()) incomingContext.withKey(Span.ContextKey, requestSpan) else incomingContext // TODO: Handle HTTP Server Metrics new HttpServer.Handler { override def context: Context = handlerContext override def span: Span = requestSpan override def finishRequest(): Unit = {} override def startResponse[HttpResponse](response: HttpResponse.Writable[HttpResponse], context: Context): HttpResponse = { if(settings.enableContextPropagation) { _propagation.writeContext(context, response, Direction.Returning) } response.build() } override def endResponse(): Unit = { span.finish() } } } private def buildServerSpan(context: Context, request: HttpRequest): Span = { val span = Kamon.buildSpan(operationName(request)) .withMetricTag("span.kind", "server") .withMetricTag("component", component) def addTag(tag: String, value: String, mode: TagMode): Unit = mode match { case TagMode.Metric => span.withMetricTag(tag, value) case TagMode.Span => span.withTag(tag, value) case TagMode.Off => } addTag("http.url", request.url, settings.urlTagMode) addTag("http.method", request.method, settings.urlTagMode) settings.contextTags.foreach { case (tagName, mode) => context.getTag(tagName).foreach(tagValue => addTag(tagName, tagValue, mode)) } span.start() } private def operationName(request: HttpRequest): String = { val requestPath = request.path val customMapping = settings.operationMappings.find { case (pattern, _) => pattern.accept(requestPath) }.map(_._2) customMapping.getOrElse("http.request") } } case class Settings( enableContextPropagation: Boolean, propagationChannel: String, enableServerMetrics: Boolean, serverMetricsTags: Seq[String], enableTracing: Boolean, traceIDTag: Option[String], enableSpanMetrics: Boolean, urlTagMode: TagMode, methodTagMode: TagMode, statusCodeTagMode: TagMode, contextTags: Map[String, TagMode], unhandledOperationName: String, operationMappings: Map[GlobPathFilter, String] ) object Settings { sealed trait TagMode object TagMode { case object Metric extends TagMode case object Span extends TagMode case object Off extends TagMode def from(value: String): TagMode = value.toLowerCase match { case "metric" => TagMode.Metric case "span" => TagMode.Span case _ => TagMode.Off } } def from(config: Config): Settings = { // Context propagation settings val enablePropagation = config.getBoolean("propagation.enabled") val propagationChannel = config.getString("propagation.channel") // HTTP Server metrics settings val enableServerMetrics = config.getBoolean("metrics.enabled") val serverMetricsTags = config.getStringList("metrics.tags").asScala // Tracing settings val enableTracing = config.getBoolean("tracing.enabled") val traceIdTag = Option(config.getString("tracing.trace-id-tag")).filterNot(_ == "none") val enableSpanMetrics = config.getBoolean("tracing.span-metrics") val urlTagMode = TagMode.from(config.getString("tracing.tags.url")) val methodTagMode = TagMode.from(config.getString("tracing.tags.method")) val statusCodeTagMode = TagMode.from(config.getString("tracing.tags.status-code")) val contextTags = config.getConfig("tracing.tags.from-context").pairs.map { case (tagName, mode) => (tagName, TagMode.from(mode)) } val unhandledOperationName = config.getString("tracing.operations.unhandled") val operationMappings = config.getConfig("tracing.operations.mappings").pairs.map { case (pattern, operationName) => (new GlobPathFilter(pattern), operationName) } Settings( enablePropagation, propagationChannel, enableServerMetrics, serverMetricsTags, enableTracing, traceIdTag, enableSpanMetrics, urlTagMode, methodTagMode, statusCodeTagMode, contextTags, unhandledOperationName, operationMappings ) } } }