├── project ├── build.properties └── plugins.sbt ├── version.sbt ├── common-tests └── src │ └── test │ ├── resources │ ├── conf │ │ ├── application-netty.conf │ │ ├── application-akka-http.conf │ │ └── application.conf │ └── logback.xml │ └── scala │ └── kamon │ └── instrumentation │ └── play │ ├── WSInstrumentationSpec.scala │ └── RequestInstrumentationSpec.scala ├── .travis.yml ├── README.md ├── LICENSE ├── .gitignore ├── kamon-play └── src │ └── main │ ├── scala │ └── kamon │ │ └── instrumentation │ │ └── play │ │ ├── GuiceModule.scala │ │ ├── PlayClientInstrumentation.scala │ │ └── PlayServerInstrumentation.scala │ └── resources │ └── reference.conf └── CONTRIBUTING.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.8 -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "2.0.1-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /common-tests/src/test/resources/conf/application-netty.conf: -------------------------------------------------------------------------------- 1 | include "application.conf" 2 | 3 | play.server.provider=play.core.server.NettyServerProvider 4 | -------------------------------------------------------------------------------- /common-tests/src/test/resources/conf/application-akka-http.conf: -------------------------------------------------------------------------------- 1 | include "application.conf" 2 | 3 | play.server.provider=play.core.server.AkkaHttpServerProvider 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | script: 3 | - sbt +test 4 | scala: 5 | - 2.12.3 6 | jdk: 7 | - openjdk8 8 | before_script: 9 | - mkdir $TRAVIS_BUILD_DIR/tmp 10 | - export SBT_OPTS="-Djava.io.tmpdir=$TRAVIS_BUILD_DIR/tmp" 11 | sudo: false 12 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | lazy val root: Project = project.in(file(".")).dependsOn(latestSbtUmbrella) 2 | lazy val latestSbtUmbrella = ProjectRef(uri("git://github.com/kamon-io/kamon-sbt-umbrella.git#kamon-2.x"), "kamon-sbt-umbrella") 3 | 4 | addSbtPlugin("com.lightbend.sbt" % "sbt-javaagent" % "0.1.4") 5 | -------------------------------------------------------------------------------- /common-tests/src/test/resources/conf/application.conf: -------------------------------------------------------------------------------- 1 | # ================================== # 2 | # kamon-play reference configuration # 3 | # ================================== # 4 | 5 | kamon { 6 | trace { 7 | tick-interval = 1 millisecond 8 | sampler = always 9 | } 10 | 11 | propagation.http.default.tags.mappings { 12 | request-id = "x-request-id" 13 | } 14 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This reporsitory has been moved. 2 | 3 | Since March 2020 all the Kamon instrumentation and reporting modules were moved to Kamon's main repository at [https://github.com/kamon-io/kamon](https://github.com/kamon-io/kamon). Please check out the main repository for the latest sources, reporting issues or start contributing. You can also stop by our [Gitter Channel](https://gitter.im/kamon-io/Kamon). -------------------------------------------------------------------------------- /common-tests/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Copyright © 2013-2014 the kamon project 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 6 | use this file except in compliance with the License. You may obtain a copy of 7 | the License at 8 | 9 | [http://www.apache.org/licenses/LICENSE-2.0] 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | License for the specific language governing permissions and limitations under 15 | the License. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | .history 4 | *.sc 5 | .pygments-cache 6 | .DS_Store 7 | .metals/ 8 | .bloop/ 9 | 10 | # sbt specific 11 | dist/* 12 | target/ 13 | lib_managed/ 14 | src_managed/ 15 | project/boot/ 16 | project/plugins/project/ 17 | .ensime 18 | .ensime_cache 19 | 20 | # Scala-IDE specific 21 | .scala_dependencies 22 | .idea 23 | .idea_modules 24 | 25 | # Intellij 26 | .idea/ 27 | *.iml 28 | *.iws 29 | 30 | # Eclipse 31 | .project 32 | .settings 33 | .classpath 34 | .cache 35 | .cache-main 36 | .cache-tests 37 | bin/ 38 | 39 | _site 40 | 41 | # Ignore Play! working directory # 42 | db 43 | eclipse 44 | lib 45 | log 46 | logs 47 | modules 48 | precompiled 49 | project/project 50 | project/target 51 | target 52 | tmp 53 | test-result 54 | server.pid 55 | *.iml 56 | *.eml 57 | 58 | # Default sigar library provision location. 59 | native/ 60 | -------------------------------------------------------------------------------- /kamon-play/src/main/scala/kamon/instrumentation/play/GuiceModule.scala: -------------------------------------------------------------------------------- 1 | package kamon.instrumentation.play 2 | 3 | import javax.inject._ 4 | import kamon.Kamon 5 | import play.api.inject.{ApplicationLifecycle, Binding, Module} 6 | import play.api.{Configuration, Environment, Logger, Mode} 7 | 8 | import scala.concurrent.Future 9 | 10 | class GuiceModule extends Module { 11 | def bindings(environment: Environment, configuration: Configuration): Seq[Binding[GuiceModule.KamonLoader]] = { 12 | Seq(bind[GuiceModule.KamonLoader].toSelf.eagerly()) 13 | } 14 | } 15 | 16 | object GuiceModule { 17 | 18 | @Singleton 19 | class KamonLoader @Inject() (lifecycle: ApplicationLifecycle, environment: Environment, configuration: Configuration) { 20 | Logger(classOf[KamonLoader]).info("Reconfiguring Kamon with Play's Config") 21 | Kamon.reconfigure(configuration.underlying) 22 | Kamon.loadModules() 23 | 24 | lifecycle.addStopHook { () => 25 | if(environment.mode != Mode.Dev) 26 | Kamon.stopModules() 27 | else 28 | Future.successful(()) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to Kamon 2 | ===================== 3 | 4 | Thanks for your intention on collaborating to the Kamon Project! It doesn't matter if you want to provide a small change 5 | to our docs, are lost in configuration or want contribute a brand new feature, we value all of your contributions and 6 | the time you take to use our tool and prepare a contribution, we only ask you to follow this guidance depending on your 7 | situation: 8 | 9 | If you are experiencing a bug 10 | ----------------------------- 11 | 12 | If you see weird exceptions in your log or something definitely is working improperly please [open an issue] and include 13 | the Kamon, Akka and Spray/Play! versions that you are using along with as many useful information you can find related 14 | to the issue. If you can provide a gist or a short way to reproduce the issue we will be more than happy! 15 | 16 | If you don't know what is wrong 17 | ------------------------------- 18 | 19 | If you don't see any metrics at all or features are not working maybe you have a setup or configuration problem, to 20 | address this kind of problems please send us a emails to our [mailing list] and we will reply as soon as we can! Again, 21 | please include the relevant version and current setup information to speed up the process. If you are in doubt of 22 | whether you have a bug or a configuration problem, email us and we will take care of openning a issue if necessary. 23 | 24 | If you want to make a code contribution to the project 25 | ------------------------------------------------------ 26 | 27 | Awesome! First, please note that we try to follow the [commit message conventions] used by the Spray guys and we need 28 | you to electronically fill our [CLA] before accepting your contribution. Also, if your PR contains various commits, 29 | please squash them into a single commit. Let the PR rain begin! 30 | 31 | 32 | [open an issue]: https://github.com/kamon-io/Kamon/issues/new 33 | [mailing list]: https://groups.google.com/forum/#!forum/kamon-user 34 | [commit message conventions]: http://spray.io/project-info/contributing/ 35 | [CLA]: https://docs.google.com/forms/d/1G_IDrBTFzOMwHvhxfKRBwNtpRelSa_MZ6jecH8lpTlc/viewform 36 | -------------------------------------------------------------------------------- /kamon-play/src/main/scala/kamon/instrumentation/play/PlayClientInstrumentation.scala: -------------------------------------------------------------------------------- 1 | package kamon.instrumentation.play 2 | 3 | import java.util.concurrent.Callable 4 | 5 | import kamon.Kamon 6 | import kamon.instrumentation.http.{HttpClientInstrumentation, HttpMessage} 7 | import kamon.util.CallingThreadExecutionContext 8 | import kanela.agent.api.instrumentation.InstrumentationBuilder 9 | import kanela.agent.libs.net.bytebuddy.implementation.bind.annotation.{RuntimeType, SuperCall} 10 | import play.api.libs.ws.{StandaloneWSRequest, StandaloneWSResponse, WSRequestExecutor, WSRequestFilter} 11 | 12 | import scala.concurrent.Future 13 | 14 | class PlayClientInstrumentation extends InstrumentationBuilder { 15 | 16 | onSubTypesOf("play.api.libs.ws.StandaloneWSClient") 17 | .intercept(method("url"), classOf[WSClientUrlInterceptor]) 18 | } 19 | 20 | class WSClientUrlInterceptor 21 | object WSClientUrlInterceptor { 22 | 23 | @RuntimeType 24 | def url(@SuperCall zuper: Callable[StandaloneWSRequest]): StandaloneWSRequest = { 25 | zuper 26 | .call() 27 | .withRequestFilter(_clientInstrumentationFilter) 28 | } 29 | 30 | @volatile private var _httpClientInstrumentation: HttpClientInstrumentation = rebuildHttpClientInstrumentation 31 | Kamon.onReconfigure(_ => _httpClientInstrumentation = rebuildHttpClientInstrumentation()) 32 | 33 | private def rebuildHttpClientInstrumentation(): HttpClientInstrumentation = { 34 | val httpClientConfig = Kamon.config().getConfig("kamon.instrumentation.play.http.client") 35 | _httpClientInstrumentation = HttpClientInstrumentation.from(httpClientConfig, "play.http.client") 36 | _httpClientInstrumentation 37 | } 38 | 39 | private val _clientInstrumentationFilter = WSRequestFilter { rf: WSRequestExecutor => 40 | new WSRequestExecutor { 41 | override def apply(request: StandaloneWSRequest): Future[StandaloneWSResponse] = { 42 | val currentContext = Kamon.currentContext() 43 | val requestHandler = _httpClientInstrumentation.createHandler(toRequestBuilder(request), currentContext) 44 | val responseFuture = Kamon.runWithSpan(requestHandler.span, finishSpan = false) { 45 | rf(requestHandler.request) 46 | } 47 | 48 | responseFuture.transform( 49 | s = response => { 50 | requestHandler.processResponse(toResponse(response)) 51 | response 52 | }, 53 | f = error => { 54 | requestHandler.span.fail(error).finish() 55 | error 56 | } 57 | )(CallingThreadExecutionContext) 58 | } 59 | } 60 | } 61 | 62 | private def toRequestBuilder(request: StandaloneWSRequest): HttpMessage.RequestBuilder[StandaloneWSRequest] = 63 | new HttpMessage.RequestBuilder[StandaloneWSRequest] { 64 | private var _newHttpHeaders: List[(String, String)] = List.empty 65 | 66 | override def write(header: String, value: String): Unit = 67 | _newHttpHeaders = (header -> value) :: _newHttpHeaders 68 | 69 | override def build(): StandaloneWSRequest = 70 | request.addHttpHeaders(_newHttpHeaders: _*) 71 | 72 | override def read(header: String): Option[String] = 73 | request.header(header) 74 | 75 | override def readAll(): Map[String, String] = 76 | request.headers.mapValues(_.head).toMap 77 | 78 | override def url: String = 79 | request.url 80 | 81 | override def path: String = 82 | request.uri.getPath 83 | 84 | override def method: String = 85 | request.method 86 | 87 | override def host: String = 88 | request.uri.getHost 89 | 90 | override def port: Int = 91 | request.uri.getPort 92 | } 93 | 94 | private def toResponse(response: StandaloneWSResponse): HttpMessage.Response = new HttpMessage.Response { 95 | override def statusCode: Int = response.status 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /common-tests/src/test/scala/kamon/instrumentation/play/WSInstrumentationSpec.scala: -------------------------------------------------------------------------------- 1 | /* ========================================================================================= 2 | * Copyright © 2013-2017 the kamon project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 11 | * either express or implied. See the License for the specific language governing permissions 12 | * and limitations under the License. 13 | * ========================================================================================= 14 | */ 15 | 16 | package kamon.instrumentation.play 17 | 18 | 19 | import kamon.Kamon 20 | import kamon.context.Context 21 | import kamon.tag.Lookups.{plain, plainLong} 22 | import kamon.testkit._ 23 | import kamon.trace.Span 24 | import org.scalatest.concurrent.{Eventually, ScalaFutures} 25 | import org.scalatest.time.SpanSugar 26 | import org.scalatest.{BeforeAndAfterAll, OptionValues} 27 | import org.scalatestplus.play.PlaySpec 28 | import org.scalatestplus.play.guice.GuiceOneServerPerSuite 29 | import play.api.Application 30 | import play.api.inject.guice.GuiceApplicationBuilder 31 | import play.api.libs.ws.WSClient 32 | import play.api.mvc.Results.{InternalServerError, NotFound, Ok} 33 | import play.api.mvc.{Action, AnyContent, DefaultActionBuilder, Handler} 34 | import play.api.test.Helpers._ 35 | import play.api.test._ 36 | import play.api.libs.ws.{WSRequestExecutor, WSRequestFilter} 37 | import play.api.mvc.Handler.Stage 38 | import play.api.routing.{HandlerDef, Router} 39 | 40 | import scala.concurrent.ExecutionContext.Implicits.global 41 | import scala.concurrent.{ExecutionContext, Future} 42 | import scala.util.{Failure, Success} 43 | 44 | 45 | class WSInstrumentationSpec extends PlaySpec with GuiceOneServerPerSuite with ScalaFutures with Eventually with SpanSugar 46 | with BeforeAndAfterAll with MetricInspection.Syntax with Reconfigure with OptionValues with TestSpanReporter { 47 | 48 | System.setProperty("config.file", "./common-tests/src/test/resources/conf/application.conf") 49 | 50 | override def fakeApplication(): Application = new GuiceApplicationBuilder() 51 | .appRoutes(testRoutes) 52 | .build 53 | 54 | def testRoutes(app: Application): PartialFunction[(String, String), Handler] = { 55 | val action = app.injector.instanceOf(classOf[DefaultActionBuilder]) 56 | 57 | { 58 | case ("GET", "/ok") => handler(action { Ok }) 59 | case ("GET", "/trace-id") => handler(action { Ok(Kamon.currentSpan().trace.id.string) }) 60 | case ("GET", "/example-tag") => handler(action { Ok(Kamon.currentContext().getTag(plain("example"))) }) 61 | case ("GET", "/error") => handler(action { InternalServerError }) 62 | case ("GET", "/inside-controller") => handler(insideController(s"http://localhost:$port/async")(app)) 63 | } 64 | } 65 | 66 | // Adds the HandlerDef attribute to the request which simulates what would happen when a generated router handles 67 | // the request. 68 | def handler[T](action: Action[T]): Handler = { 69 | Stage.modifyRequest(req => { 70 | req.addAttr(Router.Attrs.HandlerDef, HandlerDef( 71 | classLoader = getClass.getClassLoader, 72 | routerPackage = "kamon", 73 | controller = "kamon.TestController", 74 | method = "testMethod", 75 | parameterTypes = Seq.empty, 76 | verb = req.method, 77 | path = req.path 78 | )) 79 | }, action) 80 | } 81 | 82 | "the WS instrumentation" should { 83 | "generate a client span for the WS request" in { 84 | val wsClient = app.injector.instanceOf[WSClient] 85 | val endpoint = s"http://localhost:$port/ok" 86 | val response = await(wsClient.url(endpoint).get()) 87 | response.status mustBe 200 88 | 89 | eventually(timeout(5 seconds)) { 90 | val span = testSpanReporter().nextSpan().value 91 | span.kind mustBe Span.Kind.Client 92 | span.operationName mustBe "GET" 93 | span.metricTags.get(plain("component")) mustBe "play.http.client" 94 | span.metricTags.get(plain("http.method")) mustBe "GET" 95 | span.metricTags.get(plainLong("http.status_code")) mustBe 200L 96 | } 97 | } 98 | 99 | "ensure that server Span has the same trace ID as the client Span" in { 100 | val wsClient = app.injector.instanceOf[WSClient] 101 | val parentSpan = Kamon.internalSpanBuilder("inside-controller-operation-span", "test").start() 102 | val endpoint = s"http://localhost:$port/trace-id" 103 | 104 | val response = Kamon.runWithSpan(parentSpan)(await(wsClient.url(endpoint).get())) 105 | response.status mustBe 200 106 | 107 | eventually(timeout(2 seconds)) { 108 | val span = testSpanReporter().nextSpan().value 109 | span.kind mustBe Span.Kind.Client 110 | span.operationName mustBe "GET" 111 | span.metricTags.get(plain("component")) mustBe "play.http.client" 112 | span.metricTags.get(plain("http.method")) mustBe "GET" 113 | span.metricTags.get(plainLong("http.status_code")) mustBe 200L 114 | 115 | response.body mustBe parentSpan.trace.id.string 116 | } 117 | } 118 | 119 | "propagate context tags" in { 120 | val wsClient = app.injector.instanceOf[WSClient] 121 | val testContext = Context.of("example", "one") 122 | val endpoint = s"http://localhost:$port/example-tag" 123 | 124 | val response = Kamon.runWithContext(testContext)(await(wsClient.url(endpoint).get())) 125 | response.status mustBe 200 126 | response.body mustBe "one" 127 | } 128 | 129 | "run the WSClient instrumentation only once, even if request filters are added" in { 130 | val wsClient = app.injector.instanceOf[WSClient] 131 | val okSpan = Kamon.internalSpanBuilder("ok-operation-span", "test").start() 132 | val endpoint = s"http://localhost:$port/ok" 133 | 134 | Kamon.runWithSpan(okSpan) { 135 | val response = await(wsClient.url(endpoint) 136 | .withRequestFilter(new DumbRequestFilter()) 137 | .get()) 138 | 139 | response.status mustBe 200 140 | } 141 | 142 | eventually(timeout(2 seconds)) { 143 | val span = testSpanReporter().nextSpan().value 144 | span.kind mustBe Span.Kind.Client 145 | span.operationName mustBe "GET" 146 | span.metricTags.get(plain("component")) mustBe "play.http.client" 147 | span.metricTags.get(plain("http.method")) mustBe "GET" 148 | span.metricTags.get(plainLong("http.status_code")) mustBe 200L 149 | } 150 | } 151 | } 152 | 153 | def insideController(url: String)(app:Application): Action[AnyContent] = { 154 | val action = app.injector.instanceOf(classOf[DefaultActionBuilder]) 155 | val wsClient = app.injector.instanceOf[WSClient] 156 | 157 | action.async { 158 | wsClient.url(url).get().map(_ => Ok("Ok")) 159 | } 160 | } 161 | 162 | class DumbRequestFilter() extends WSRequestFilter { 163 | def apply(executor: WSRequestExecutor) = WSRequestExecutor { request => 164 | executor(request) andThen { 165 | case Success(_) => 166 | case Failure(_) => 167 | } 168 | } 169 | } 170 | } 171 | 172 | 173 | -------------------------------------------------------------------------------- /common-tests/src/test/scala/kamon/instrumentation/play/RequestInstrumentationSpec.scala: -------------------------------------------------------------------------------- 1 | /* ========================================================================================= 2 | * Copyright © 2013-2017 the kamon project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the 10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 11 | * either express or implied. See the License for the specific language governing permissions 12 | * and limitations under the License. 13 | * ========================================================================================= 14 | */ 15 | 16 | package kamon.instrumentation.play 17 | 18 | import kamon.Kamon 19 | import kamon.context.Context 20 | import kamon.tag.Lookups._ 21 | import kamon.testkit.{MetricInspection, TestSpanReporter} 22 | import kamon.trace.Span 23 | import org.scalatest.concurrent.{Eventually, ScalaFutures} 24 | import org.scalatest.time.SpanSugar 25 | import org.scalatest.{BeforeAndAfterAll, OptionValues} 26 | import org.scalatestplus.play._ 27 | import org.scalatestplus.play.guice.GuiceOneServerPerSuite 28 | import play.api.Application 29 | import play.api.inject.guice.GuiceApplicationBuilder 30 | import play.api.libs.ws.WSClient 31 | import play.api.mvc.Handler.Stage 32 | import play.api.mvc.Results.{NotFound, Ok} 33 | import play.api.mvc._ 34 | import play.api.mvc.request.RequestAttrKey 35 | import play.api.routing.{HandlerDef, Router} 36 | import play.api.test.Helpers._ 37 | import play.api.test._ 38 | 39 | import scala.concurrent.{ExecutionContext, Future} 40 | 41 | class AkkaHTTPRequestHandlerInstrumentationSpec extends { 42 | val confFile = "/common-tests/src/test/resources/conf/application-akka-http.conf" 43 | //https://www.playframaework.com/documentation/2.6.x/NettyServer#Verifying-that-the-Netty-server-is-running 44 | //The Akka HTTP backend will not set a value for this request attribute. 45 | val expectedServer = "play.server.akka-http" 46 | } with RequestHandlerInstrumentationSpec 47 | 48 | class NettyRequestHandlerInstrumentationSpec extends { 49 | val confFile = "/common-tests/src/test/resources/conf/application-netty.conf" 50 | val expectedServer = "play.server.netty" 51 | } with RequestHandlerInstrumentationSpec 52 | 53 | abstract class RequestHandlerInstrumentationSpec extends PlaySpec with GuiceOneServerPerSuite with ScalaFutures 54 | with Eventually with SpanSugar with BeforeAndAfterAll with MetricInspection.Syntax with OptionValues with TestSpanReporter { 55 | 56 | val confFile: String 57 | val expectedServer: String 58 | 59 | System.setProperty("config.file", System.getProperty("user.dir") + confFile) 60 | 61 | implicit val executor: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global 62 | 63 | def testRoutes(app: Application): PartialFunction[(String, String), Handler] = { 64 | val action = app.injector.instanceOf(classOf[DefaultActionBuilder]) 65 | 66 | { 67 | case ("GET", "/ok") => handler(action { Ok }) 68 | case ("GET", "/request-id") => handler(action { Ok(Kamon.currentContext().getTag(option("request-id")).getOrElse("undefined")) }) 69 | case ("GET", "/async") => handler(action.async { Future { Ok } }) 70 | case ("GET", "/not-found") => handler(action { NotFound }) 71 | case ("GET", "/server") => handler(action { req => Ok(serverImplementationName(req)) }) 72 | case ("GET", "/error") => handler(action(_ => sys.error("This page generates an error!"))) 73 | } 74 | } 75 | 76 | def serverImplementationName(req: Request[AnyContent]): String = { 77 | req.attrs.get(RequestAttrKey.Server) 78 | .map(s => if(s == "netty") "play.server.netty" else "unknown") 79 | .getOrElse("play.server.akka-http") 80 | } 81 | 82 | // Adds the HandlerDef attribute to the request which simulates what would happen when a generated router handles 83 | // the request. 84 | def handler[T](action: Action[T]): Handler = { 85 | Stage.modifyRequest(req => { 86 | req.addAttr(Router.Attrs.HandlerDef, HandlerDef( 87 | classLoader = getClass.getClassLoader, 88 | routerPackage = "kamon", 89 | controller = "kamon.TestController", 90 | method = "testMethod", 91 | parameterTypes = Seq.empty, 92 | verb = req.method, 93 | path = req.path 94 | )) 95 | }, action) 96 | } 97 | 98 | override def fakeApplication(): Application = new GuiceApplicationBuilder() 99 | .appRoutes(testRoutes) 100 | .build 101 | 102 | "the Request instrumentation" should { 103 | "check the right server is configured" in { 104 | val wsClient = app.injector.instanceOf[WSClient] 105 | val endpoint = s"http://localhost:$port/server" 106 | 107 | val response = await(wsClient.url(endpoint).get()) 108 | response.status mustBe 200 109 | response.body mustBe expectedServer 110 | } 111 | 112 | "create server Spans for served operations" in { 113 | val wsClient = app.injector.instanceOf[WSClient] 114 | val endpoint = s"http://localhost:$port/ok" 115 | val response = await(wsClient.url(endpoint).get()) 116 | response.status mustBe 200 117 | 118 | eventually(timeout(5 seconds)) { 119 | val span = testSpanReporter().nextSpan().value 120 | span.operationName mustBe "/ok" 121 | span.metricTags.get(plain("component")) mustBe expectedServer 122 | span.metricTags.get(plain("http.method")) mustBe "GET" 123 | span.metricTags.get(plainLong("http.status_code")) mustBe 200L 124 | } 125 | } 126 | 127 | "read headers case-insensitively" in { 128 | val wsClient = app.injector.instanceOf[WSClient] 129 | val endpoint = s"http://localhost:$port/request-id" 130 | 131 | { 132 | val response = await(wsClient.url(endpoint).addHttpHeaders("x-request-id" -> "123456").get()) 133 | response.status mustBe 200 134 | response.body mustBe "123456" 135 | } 136 | { 137 | val response = await(wsClient.url(endpoint).addHttpHeaders("x-reQUest-Id" -> "654321").get()) 138 | response.status mustBe 200 139 | response.body mustBe "654321" 140 | } 141 | } 142 | 143 | "mark spans as failed when there is an error processing the request" in { 144 | val wsClient = app.injector.instanceOf[WSClient] 145 | val endpoint = s"http://localhost:$port/error" 146 | val response = await(wsClient.url(endpoint).get()) 147 | response.status mustBe 500 148 | 149 | eventually(timeout(5 seconds)) { 150 | val span = testSpanReporter().nextSpan().value 151 | span.operationName mustBe "/error" 152 | span.hasError mustBe true 153 | span.metricTags.get(plain("component")) mustBe expectedServer 154 | span.metricTags.get(plain("http.method")) mustBe "GET" 155 | span.metricTags.get(plainLong("http.status_code")) mustBe 500L 156 | } 157 | } 158 | 159 | "include the trace id in responses" in { 160 | val wsClient = app.injector.instanceOf[WSClient] 161 | val endpoint = s"http://localhost:$port/ok" 162 | val parentSpan = Kamon.internalSpanBuilder("client-parent", "play.test").start() 163 | val response = Kamon.runWithSpan(parentSpan, finishSpan = false)(await(wsClient.url(endpoint).get())) 164 | response.status mustBe 200 165 | 166 | eventually(timeout(5 seconds)) { 167 | val span = testSpanReporter().nextSpan().value 168 | span.operationName mustBe "/ok" 169 | span.metricTags.get(plain("component")) mustBe expectedServer 170 | span.metricTags.get(plain("http.method")) mustBe "GET" 171 | span.metricTags.get(plainLong("http.status_code")) mustBe 200L 172 | 173 | span.trace.id mustBe parentSpan.trace.id 174 | response.header("trace-id").value mustBe parentSpan.trace.id.string 175 | response.header("span-id") mustBe empty 176 | } 177 | } 178 | } 179 | } -------------------------------------------------------------------------------- /kamon-play/src/main/scala/kamon/instrumentation/play/PlayServerInstrumentation.scala: -------------------------------------------------------------------------------- 1 | package kamon.instrumentation.play 2 | 3 | import java.time.Duration 4 | import java.util.concurrent.atomic.AtomicLong 5 | 6 | import io.netty.channel.Channel 7 | import io.netty.handler.codec.http.{HttpRequest, HttpResponse} 8 | import io.netty.util.concurrent.GenericFutureListener 9 | import kamon.Kamon 10 | import kamon.context.Storage 11 | import kamon.instrumentation.akka.http.ServerFlowWrapper 12 | import kamon.instrumentation.context.{CaptureCurrentTimestampOnExit, HasTimestamp} 13 | import kamon.instrumentation.http.HttpServerInstrumentation.RequestHandler 14 | import kamon.instrumentation.http.{HttpMessage, HttpServerInstrumentation} 15 | import kamon.util.CallingThreadExecutionContext 16 | import kanela.agent.api.instrumentation.InstrumentationBuilder 17 | import kanela.agent.api.instrumentation.classloader.ClassRefiner 18 | import kanela.agent.api.instrumentation.mixin.Initializer 19 | import kanela.agent.libs.net.bytebuddy.asm.Advice 20 | import play.api.mvc.RequestHeader 21 | import play.api.routing.{HandlerDef, Router} 22 | import play.core.server.NettyServer 23 | 24 | import scala.collection.JavaConverters.asScalaBufferConverter 25 | import scala.collection.concurrent.TrieMap 26 | import scala.util.{Failure, Success} 27 | 28 | class PlayServerInstrumentation extends InstrumentationBuilder { 29 | 30 | /** 31 | * When using the Akka HTTP server, we will use the exact same instrumentation that comes from the Akka HTTP module, 32 | * the only difference here is that we will change the component name. 33 | */ 34 | private val isAkkaHttpAround = ClassRefiner.builder().mustContains("play.core.server.AkkaHttpServerProvider").build() 35 | 36 | onType("play.core.server.AkkaHttpServer") 37 | .when(isAkkaHttpAround) 38 | .advise(anyMethods("createServerBinding", "play$core$server$AkkaHttpServer$$createServerBinding"), CreateServerBindingAdvice) 39 | 40 | 41 | /** 42 | * When using the Netty HTTP server we are rolling our own instrumentation which simply requires us to create the 43 | * HttpServerInstrumentation instance and call the expected callbacks on it. 44 | */ 45 | private val isNettyAround = ClassRefiner.builder().mustContains("play.core.server.NettyServerProvider").build() 46 | 47 | onType("play.core.server.NettyServer") 48 | .when(isNettyAround) 49 | .mixin(classOf[HasServerInstrumentation.Mixin]) 50 | .advise(isConstructor, NettyServerInitializationAdvice) 51 | 52 | onType("play.core.server.netty.PlayRequestHandler") 53 | .when(isNettyAround) 54 | .mixin(classOf[HasServerInstrumentation.Mixin]) 55 | .mixin(classOf[HasTimestamp.Mixin]) 56 | .advise(isConstructor, PlayRequestHandlerConstructorAdvice) 57 | .advise(isConstructor, CaptureCurrentTimestampOnExit) 58 | .advise(method("handle"), NettyPlayRequestHandlerHandleAdvice) 59 | 60 | /** 61 | * This final bit ensures that we will apply an operation name right before filters get to execute. 62 | */ 63 | onType("play.api.http.DefaultHttpRequestHandler") 64 | .advise(method("filterHandler").and(takesArguments(2)), GenerateOperationNameOnFilterHandler) 65 | } 66 | 67 | 68 | object CreateServerBindingAdvice { 69 | 70 | @Advice.OnMethodEnter 71 | def enter(): Unit = 72 | ServerFlowWrapper.changeSettings("play.server.akka-http", "kamon.instrumentation.play.http.server") 73 | 74 | @Advice.OnMethodExit 75 | def exit(): Unit = 76 | ServerFlowWrapper.resetSettings() 77 | 78 | } 79 | 80 | object NettyServerInitializationAdvice { 81 | 82 | @Advice.OnMethodExit 83 | def exit(@Advice.This server: NettyServer): Unit = { 84 | val serverWithInstrumentation = server.asInstanceOf[HasServerInstrumentation] 85 | val config = Kamon.config().getConfig("kamon.instrumentation.play.http.server") 86 | val instrumentation = HttpServerInstrumentation.from( 87 | config, 88 | component = "play.server.netty", 89 | interface = server.mainAddress.getHostName, 90 | port = server.mainAddress.getPort 91 | ) 92 | 93 | serverWithInstrumentation.setServerInstrumentation(instrumentation) 94 | } 95 | } 96 | 97 | object NettyPlayRequestHandlerHandleAdvice { 98 | 99 | @Advice.OnMethodEnter 100 | def enter(@Advice.This requestHandler: Any, @Advice.Argument(0) channel: Channel, @Advice.Argument(1) request: HttpRequest): RequestProcessingContext = { 101 | val playRequestHandler = requestHandler.asInstanceOf[HasServerInstrumentation] 102 | val serverInstrumentation = playRequestHandler.serverInstrumentation() 103 | 104 | val serverRequestHandler = serverInstrumentation.createHandler( 105 | request = toRequest(request, serverInstrumentation.interface(), serverInstrumentation.port()), 106 | deferSamplingDecision = true 107 | ) 108 | 109 | if(!playRequestHandler.hasBeenUsedBefore()) { 110 | playRequestHandler.markAsUsed() 111 | channel.closeFuture().addListener(new GenericFutureListener[io.netty.util.concurrent.Future[_ >: Void]] { 112 | override def operationComplete(future: io.netty.util.concurrent.Future[_ >: Void]): Unit = { 113 | val connectionEstablishedTime = Kamon.clock().toInstant(playRequestHandler.asInstanceOf[HasTimestamp].timestamp) 114 | val aliveTime = Duration.between(connectionEstablishedTime, Kamon.clock().instant()) 115 | serverInstrumentation.connectionClosed(aliveTime, playRequestHandler.handledRequests()) 116 | } 117 | }) 118 | } 119 | 120 | playRequestHandler.requestHandled() 121 | RequestProcessingContext(serverRequestHandler, Kamon.storeContext(serverRequestHandler.context)) 122 | } 123 | 124 | @Advice.OnMethodExit 125 | def exit(@Advice.Enter rpContext: RequestProcessingContext, @Advice.Return result: scala.concurrent.Future[HttpResponse]): Unit = { 126 | val reqHandler = rpContext.requestHandler 127 | 128 | result.onComplete { 129 | case Success(value) => 130 | reqHandler.buildResponse(toResponse(value), rpContext.scope.context) 131 | reqHandler.responseSent() 132 | 133 | case Failure(exception) => 134 | reqHandler.span.fail(exception) 135 | reqHandler.responseSent() 136 | 137 | }(CallingThreadExecutionContext) 138 | 139 | rpContext.scope.close() 140 | } 141 | 142 | case class RequestProcessingContext(requestHandler: RequestHandler, scope: Storage.Scope) 143 | 144 | private def toRequest(request: HttpRequest, serverHost: String, serverPort: Int): HttpMessage.Request = new HttpMessage.Request { 145 | override def url: String = request.uri() 146 | override def path: String = request.uri() 147 | override def method: String = request.method().name() 148 | override def host: String = serverHost 149 | override def port: Int = serverPort 150 | 151 | override def read(header: String): Option[String] = 152 | Option(request.headers().get(header)) 153 | 154 | override def readAll(): Map[String, String] = 155 | request.headers().entries().asScala.map(e => e.getKey -> e.getValue).toMap 156 | } 157 | 158 | private def toResponse(response: HttpResponse): HttpMessage.ResponseBuilder[HttpResponse] = new HttpMessage.ResponseBuilder[HttpResponse] { 159 | override def build(): HttpResponse = 160 | response 161 | 162 | override def statusCode: Int = 163 | response.status().code() 164 | 165 | override def write(header: String, value: String): Unit = 166 | response.headers().add(header, value) 167 | } 168 | } 169 | 170 | object PlayRequestHandlerConstructorAdvice { 171 | 172 | @Advice.OnMethodExit 173 | def exit(@Advice.This playRequestHandler: HasServerInstrumentation, @Advice.Argument(0) server: Any): Unit = { 174 | val instrumentation = server.asInstanceOf[HasServerInstrumentation].serverInstrumentation() 175 | playRequestHandler.setServerInstrumentation(instrumentation) 176 | instrumentation.connectionOpened() 177 | } 178 | } 179 | 180 | 181 | trait HasServerInstrumentation { 182 | def serverInstrumentation(): HttpServerInstrumentation 183 | def setServerInstrumentation(serverInstrumentation: HttpServerInstrumentation): Unit 184 | def hasBeenUsedBefore(): Boolean 185 | def markAsUsed(): Unit 186 | def requestHandled(): Unit 187 | def handledRequests(): Long 188 | } 189 | 190 | object HasServerInstrumentation { 191 | 192 | class Mixin(var serverInstrumentation: HttpServerInstrumentation, var hasBeenUsedBefore: Boolean) extends HasServerInstrumentation { 193 | private var _handledRequests: AtomicLong = null 194 | 195 | override def setServerInstrumentation(serverInstrumentation: HttpServerInstrumentation): Unit = 196 | this.serverInstrumentation = serverInstrumentation 197 | 198 | override def markAsUsed(): Unit = 199 | this.hasBeenUsedBefore = true 200 | 201 | override def requestHandled(): Unit = 202 | this._handledRequests.incrementAndGet() 203 | 204 | override def handledRequests(): Long = 205 | this._handledRequests.get() 206 | 207 | @Initializer 208 | def init(): Unit = { 209 | _handledRequests = new AtomicLong() 210 | } 211 | } 212 | } 213 | 214 | 215 | object GenerateOperationNameOnFilterHandler { 216 | 217 | private val _operationNameCache = TrieMap.empty[String, String] 218 | private val _normalizePattern = """\$([^<]+)<[^>]+>""".r 219 | 220 | @Advice.OnMethodEnter 221 | def enter(@Advice.Argument(0) request: RequestHeader): Unit = { 222 | request.attrs.get(Router.Attrs.HandlerDef).map(handler => { 223 | val span = Kamon.currentSpan() 224 | span.name(generateOperationName(handler)) 225 | span.takeSamplingDecision() 226 | }) 227 | } 228 | 229 | private def generateOperationName(handlerDef: HandlerDef): String = 230 | _operationNameCache.getOrElseUpdate(handlerDef.path, { 231 | // Convert paths of form /foo/bar/$paramname/blah to /foo/bar/paramname/blah 232 | _normalizePattern.replaceAllIn(handlerDef.path, "$1") 233 | }) 234 | 235 | } 236 | -------------------------------------------------------------------------------- /kamon-play/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | # ================================== # 2 | # kamon-play reference configuration # 3 | # ================================== # 4 | 5 | kamon.instrumentation.play.http { 6 | server { 7 | 8 | # 9 | # Configuration for HTTP context propagation. 10 | # 11 | propagation { 12 | 13 | # Enables or disables HTTP context propagation on this HTTP server instrumentation. Please note that if 14 | # propagation is disabled then some distributed tracing features will not be work as expected (e.g. Spans can 15 | # be created and reported but will not be linked across boundaries nor take trace identifiers from tags). 16 | #enabled = yes 17 | 18 | # HTTP propagation channel to b used by this instrumentation. Take a look at the kamon.propagation.http.default 19 | # configuration for more details on how to configure the detault HTTP context propagation. 20 | #channel = "default" 21 | } 22 | 23 | 24 | # 25 | # Configuration for HTTP server metrics collection. 26 | # 27 | metrics { 28 | 29 | # Enables collection of HTTP server metrics. When enabled the following metrics will be collected, assuming 30 | # that the instrumentation is fully compliant: 31 | # 32 | # - http.server.requets 33 | # - http.server.request.active 34 | # - http.server.request.size 35 | # - http.server.response.size 36 | # - http.server.connection.lifetime 37 | # - http.server.connection.usage 38 | # - http.server.connection.open 39 | # 40 | # All metrics have at least three tags: component, interface and port. Additionally, the http.server.requests 41 | # metric will also have a status_code tag with the status code group (1xx, 2xx and so on). 42 | # 43 | #enabled = yes 44 | } 45 | 46 | 47 | # 48 | # Configuration for HTTP request tracing. 49 | # 50 | tracing { 51 | 52 | # Enables HTTP request tracing. When enabled the instrumentation will create Spans for incoming requests 53 | # and finish them when the response is sent back to the clients. 54 | #enabled = yes 55 | 56 | # Select a context tag that provides a preferred trace identifier. The preferred trace identifier will be used 57 | # only if all these conditions are met: 58 | # - the context tag is present. 59 | # - there is no parent Span on the incoming context (i.e. this is the first service on the trace). 60 | # - the identifier is valid in accordance to the identity provider. 61 | #preferred-trace-id-tag = "none" 62 | 63 | # Enables collection of span metrics using the `span.processing-time` metric. 64 | #span-metrics = on 65 | 66 | # Select which tags should be included as span and span metric tags. The possible options are: 67 | # - span: the tag is added as a Span tag (i.e. using span.tag(...)) 68 | # - metric: the tag is added a a Span metric tag (i.e. using span.tagMetric(...)) 69 | # - off: the tag is not used. 70 | # 71 | tags { 72 | 73 | # Use the http.url tag. 74 | #url = span 75 | 76 | # Use the http.method tag. 77 | #method = metric 78 | 79 | # Use the http.status_code tag. 80 | #status-code = metric 81 | 82 | # Copy tags from the context into the Spans with the specified purpouse. For example, to copy a customer_type 83 | # tag from the context into the HTTP Server Span created by the instrumentation, the following configuration 84 | # should be added: 85 | # 86 | # from-context { 87 | # customer_type = span 88 | # } 89 | # 90 | from-context { 91 | 92 | } 93 | } 94 | 95 | # Controls writing trace and span identifiers to HTTP response headers sent by the instrumented servers. The 96 | # configuration can be set to either "none" to disable writing the identifiers on the response headers or to 97 | # the header name to be used when writing the identifiers. 98 | response-headers { 99 | 100 | # HTTP response header name for the trace identifier, or "none" to disable it. 101 | #trace-id = "trace-id" 102 | 103 | # HTTP response header name for the server span identifier, or "none" to disable it. 104 | #span-id = none 105 | } 106 | 107 | # Custom mappings between routes and operation names. 108 | operations { 109 | 110 | # The default operation name to be used when creating Spans to handle the HTTP server requests. In most 111 | # cases it is not possible to define an operation name right at the moment of starting the HTTP server Span 112 | # and in those cases, this operation name will be initially assigned to the Span. Instrumentation authors 113 | # should do their best effort to provide a suitable operation name or make use of the "mappings" facilities. 114 | #default = "http.server.request" 115 | 116 | # The operation name to be assigned when an application cannot find any route/endpoint/controller to handle 117 | # a given request. Depending on the instrumented framework, it might be possible to apply this operation 118 | # name automatically or not, check the frameworks' instrumentation docs for more details. 119 | #unhandled = "unhandled" 120 | 121 | # Provides custom mappings from HTTP paths into operation names. Meant to be used in cases where the bytecode 122 | # instrumentation is not able to provide a sensible operation name that is free of high cardinality values. 123 | # For example, with the following configuration: 124 | # mappings { 125 | # "/organization/*/user/*/profile" = "/organization/:orgID/user/:userID/profile" 126 | # "/events/*/rsvps" = "EventRSVPs" 127 | # } 128 | # 129 | # Requests to "/organization/3651/user/39652/profile" and "/organization/22234/user/54543/profile" will have 130 | # the same operation name "/organization/:orgID/user/:userID/profile". 131 | # 132 | # Similarly, requests to "/events/aaa-bb-ccc/rsvps" and "/events/1234/rsvps" will have the same operation 133 | # name "EventRSVPs". 134 | # 135 | # The patterns are expressed as globs and the operation names are free form. 136 | # 137 | mappings { 138 | 139 | } 140 | } 141 | } 142 | } 143 | 144 | client { 145 | 146 | # 147 | # Configuration for HTTP context propagation. 148 | # 149 | propagation { 150 | 151 | # Enables or disables HTTP context propagation on this HTTP server instrumentation. Please note that if 152 | # propagation is disabled then some distributed tracing features will not be work as expected (e.g. Spans can 153 | # be created and reported but will not be linked across boundaries nor take trace identifiers from tags). 154 | #enabled = yes 155 | 156 | # HTTP propagation channel to b used by this instrumentation. Take a look at the kamon.propagation.http.default 157 | # configuration for more details on how to configure the detault HTTP context propagation. 158 | #channel = "default" 159 | } 160 | 161 | tracing { 162 | 163 | # Enables HTTP request tracing. When enabled the instrumentation will create Spans for outgoing requests 164 | # and finish them when the response is received from the server. 165 | #enabled = yes 166 | 167 | # Enables collection of span metrics using the `span.processing-time` metric. 168 | #span-metrics = on 169 | 170 | # Select which tags should be included as span and span metric tags. The possible options are: 171 | # - span: the tag is added as a Span tag (i.e. using span.tag(...)) 172 | # - metric: the tag is added a a Span metric tag (i.e. using span.tagMetric(...)) 173 | # - off: the tag is not used. 174 | # 175 | tags { 176 | 177 | # Use the http.url tag. 178 | #url = span 179 | 180 | # Use the http.method tag. 181 | #method = metric 182 | 183 | # Use the http.status_code tag. 184 | #status-code = metric 185 | 186 | # Copy tags from the context into the Spans with the specified purpouse. For example, to copy a customer_type 187 | # tag from the context into the HTTP Server Span created by the instrumentation, the following configuration 188 | # should be added: 189 | # 190 | # from-context { 191 | # customer_type = span 192 | # } 193 | # 194 | from-context { 195 | 196 | } 197 | } 198 | 199 | operations { 200 | 201 | # The default operation name to be used when creating Spans to handle the HTTP client requests. The HTTP 202 | # Client instrumentation will always try to use the HTTP Operation Name Generator configured bellow to get 203 | # a name, but if it fails to generate it then this name will be used. 204 | #default = "http.client.request" 205 | 206 | # FQCN for a HttpOperationNameGenerator implementation, or ony of the following shorthand forms: 207 | # - hostname: Uses the request Host as the operation name. 208 | # - method: Uses the request HTTP method as the operation name. 209 | # 210 | #name-generator = "method" 211 | } 212 | } 213 | } 214 | } 215 | 216 | # Registers the Guice module on Play, which ensures that Kamon will be reconfigured with Play's configuration 217 | # and all Kamon modules will be started appropriately. 218 | play.modules.enabled += "kamon.instrumentation.play.GuiceModule" 219 | 220 | kanela.modules { 221 | play-framework { 222 | order = 2 223 | name = "Play Framework Instrumentation" 224 | description = "Provides context propagation, distributed tracing and HTTP client and server metrics for Play Framework" 225 | 226 | instrumentations = [ 227 | "kamon.instrumentation.play.PlayServerInstrumentation", 228 | "kamon.instrumentation.play.PlayClientInstrumentation" 229 | ] 230 | 231 | within = [ 232 | "^play.*" 233 | ] 234 | } 235 | } --------------------------------------------------------------------------------