├── 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 | }
--------------------------------------------------------------------------------