├── project ├── build.properties └── plugins.sbt ├── version.sbt ├── kamon-akka-http └── src │ ├── test │ ├── resources │ │ ├── https │ │ │ ├── rootCA.srl │ │ │ ├── server.p12 │ │ │ ├── server.csr │ │ │ ├── server.crt │ │ │ ├── rootCA.crt │ │ │ ├── rootCA.key │ │ │ ├── server.key │ │ │ └── chain.pem │ │ ├── logback.xml │ │ └── application.conf │ └── scala │ │ └── kamon │ │ ├── akka │ │ └── http │ │ │ ├── ServerFlowWrapperSpec.scala │ │ │ ├── AkkaHttpServerMetricsSpec.scala │ │ │ ├── FastFutureInstrumentationSpec.scala │ │ │ ├── AkkaHttpClientTracingSpec.scala │ │ │ └── AkkaHttpServerTracingSpec.scala │ │ └── testkit │ │ ├── TestNameGenerator.scala │ │ └── TestWebServer.scala │ └── main │ ├── java │ └── kamon │ │ └── instrumentation │ │ └── akka │ │ └── http │ │ ├── HttpExtBindAndHandleAdvice.java │ │ ├── Http2ExtBindAndHandleAdvice.java │ │ └── HttpExtSingleRequestAdvice.java │ ├── scala │ └── kamon │ │ └── instrumentation │ │ └── akka │ │ └── http │ │ ├── TracingDirectives.scala │ │ ├── AkkaHttpClientInstrumentation.scala │ │ ├── AkkaHttpInstrumentation.scala │ │ └── ServerFlowWrapper.scala │ ├── resources │ └── reference.conf │ ├── scala-2.12 │ └── kamon │ │ └── instrumentation │ │ └── akka │ │ └── http │ │ └── AkkaHttpServerInstrumentation.scala │ ├── scala-2.11 │ └── kamon │ │ └── instrumentation │ │ └── akka │ │ └── http │ │ └── AkkaHttpServerInstrumentation.scala │ └── scala-2.13 │ └── kamon │ └── instrumentation │ └── akka │ └── http │ └── AkkaHttpServerInstrumentation.scala ├── .travis.yml ├── release-notes.md ├── README.md ├── .gitignore └── LICENSE /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.8 2 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "2.0.4-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /kamon-akka-http/src/test/resources/https/rootCA.srl: -------------------------------------------------------------------------------- 1 | 20558643DB2ECACFBC7E638CEE3EBBD3BAADC473 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | lazy val root = project in file(".") dependsOn(RootProject(uri("git://github.com/kamon-io/kamon-sbt-umbrella.git#kamon-2.x"))) -------------------------------------------------------------------------------- /kamon-akka-http/src/test/resources/https/server.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamon-io/kamon-akka-http/HEAD/kamon-akka-http/src/test/resources/https/server.p12 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | script: 3 | - sbt +test 4 | scala: 5 | - 2.12.8 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 | 13 | -------------------------------------------------------------------------------- /release-notes.md: -------------------------------------------------------------------------------- 1 | kamon-akka-http release notes 2 | ------------------------ 3 | 4 | ### 6. Jun 2017: version 0.6.7 5 | 6 | - Properly handle completion state of the connection flow - by @jypma 7 | 8 | 9 | ### 14. Jun 2017: version 0.6.8 10 | 11 | - Bump Up all `kamon` dependencies to `0.6.7` 12 | 13 | - Fixed README collapse - by @petitviolet 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). -------------------------------------------------------------------------------- /kamon-akka-http/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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | .history 4 | *.sc 5 | .pygments-cache 6 | .DS_Store 7 | .metals 8 | 9 | # sbt specific 10 | dist/* 11 | target/ 12 | lib_managed/ 13 | src_managed/ 14 | project/boot/ 15 | project/plugins/project/ 16 | 17 | # Scala-IDE specific 18 | .scala_dependencies 19 | .idea 20 | .idea_modules 21 | 22 | # Intellij 23 | .idea/ 24 | *.iml 25 | *.iws 26 | 27 | # Eclipse 28 | .project 29 | .settings 30 | .classpath 31 | .cache 32 | bin/ 33 | 34 | _site 35 | 36 | # Ignore Play! working directory # 37 | db 38 | eclipse 39 | lib 40 | log 41 | logs 42 | modules 43 | precompiled 44 | project/project 45 | project/target 46 | target 47 | tmp 48 | test-result 49 | server.pid 50 | *.iml 51 | *.eml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Copyright © 2013-2016 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. 16 | -------------------------------------------------------------------------------- /kamon-akka-http/src/main/java/kamon/instrumentation/akka/http/HttpExtBindAndHandleAdvice.java: -------------------------------------------------------------------------------- 1 | package kamon.instrumentation.akka.http; 2 | 3 | import akka.NotUsed; 4 | import akka.http.scaladsl.model.HttpRequest; 5 | import akka.http.scaladsl.model.HttpResponse; 6 | import akka.stream.scaladsl.Flow; 7 | import kanela.agent.libs.net.bytebuddy.asm.Advice; 8 | 9 | public class HttpExtBindAndHandleAdvice { 10 | 11 | @Advice.OnMethodEnter(suppress = Throwable.class) 12 | public static void onEnter(@Advice.Argument(value = 0, readOnly = false) Flow handler, 13 | @Advice.Argument(1) String iface, 14 | @Advice.Argument(2) Integer port) { 15 | 16 | handler = ServerFlowWrapper.apply(handler, iface, port); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /kamon-akka-http/src/main/java/kamon/instrumentation/akka/http/Http2ExtBindAndHandleAdvice.java: -------------------------------------------------------------------------------- 1 | package kamon.instrumentation.akka.http; 2 | 3 | import akka.http.scaladsl.model.HttpRequest; 4 | import akka.http.scaladsl.model.HttpResponse; 5 | import kanela.agent.libs.net.bytebuddy.asm.Advice; 6 | import scala.Function1; 7 | import scala.concurrent.Future; 8 | 9 | public class Http2ExtBindAndHandleAdvice { 10 | 11 | @Advice.OnMethodEnter(suppress = Throwable.class) 12 | public static void onEnter(@Advice.Argument(value = 0, readOnly = false) Function1> handler, 13 | @Advice.Argument(1) String iface, 14 | @Advice.Argument(2) Integer port) { 15 | 16 | handler = new Http2BlueprintInterceptor.HandlerWithEndpoint(iface, port, handler); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /kamon-akka-http/src/test/resources/https/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICszCCAZsCAQAwWDELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx 3 | ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDERMA8GA1UEAwwIa2Ft 4 | b24uaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpohG24Gs/icyw 5 | htIA3D1hRaHnMhCtfAMK6yy8Es/gtz4b2Ahr/bP6KmxAH2F+C3ha2lQfRaKToQWL 6 | aQLwtSqH8+b1YoBTcflqtPX6LR3Sk8MBxGtYCXMyInN3qJyljS0zOhntL+y2P2/W 7 | fZQd99U0VNq3jxmGSHjvQ0RerJ4y2LdWF6ai3+yktFHvK2SmvGNXzFLzkgEwl9fP 8 | FwyB2pWzYl+D4fmyTx7CQxjbyGOf+z1V+umxoxDkqB6L2btOJrZ+4nDFIduEBdWH 9 | 0xGBe8lXMuxD9foy2B7wWN3IB7dY9Z9UrVTEFbbQ9fU9+3QWEyx5SEzuQDbYEwGz 10 | HppEt7zLAgMBAAGgFjAUBgkqhkiG9w0BCQcxBwwFa2Ftb24wDQYJKoZIhvcNAQEL 11 | BQADggEBAJd3GMvju7earx/3sd0BxUzFIqyeQToGy34ie6WP6ZuF+RhVR0sfXETF 12 | QMCkvthEgGNi/4GOCcK1LLCVET9sz+iMMxx0gQddcNB1jbxaU4/MNvBR8oxf9YNu 13 | zcmgO1wHKPK0TKbRJtB7MGyKRsKLa4wZ5lEVxKTuKUdURbBOJdETvS1Wy3uJaK0m 14 | LlzEu7IVJNIM9M21BsZvu8dK8jxMhJBkfWjE6du1oTJHYPQgwLipcxKBxtE2Jren 15 | 32lm5yyWmsNlQ8Y7pV7PME2HeJ33Fmhg7YVI6Vo06HglFz4ENWmC2t/JjBGgui6y 16 | MVl69Qx69j5L7RkL+jWZrvMWCMNg1Qg= 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /kamon-akka-http/src/test/resources/https/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDJDCCAgwCFCBVhkPbLsrPvH5jjO4+u9O6rcRzMA0GCSqGSIb3DQEBCwUAMEUx 3 | CzAJBgNVBAYTAkhSMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl 4 | cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTkwODEwMDgyODU3WhcNMjkwNTA5MDgy 5 | ODU3WjBYMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE 6 | CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMREwDwYDVQQDDAhrYW1vbi5pbzCC 7 | ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKmiEbbgaz+JzLCG0gDcPWFF 8 | oecyEK18AwrrLLwSz+C3PhvYCGv9s/oqbEAfYX4LeFraVB9FopOhBYtpAvC1Kofz 9 | 5vVigFNx+Wq09fotHdKTwwHEa1gJczIic3eonKWNLTM6Ge0v7LY/b9Z9lB331TRU 10 | 2rePGYZIeO9DRF6snjLYt1YXpqLf7KS0Ue8rZKa8Y1fMUvOSATCX188XDIHalbNi 11 | X4Ph+bJPHsJDGNvIY5/7PVX66bGjEOSoHovZu04mtn7icMUh24QF1YfTEYF7yVcy 12 | 7EP1+jLYHvBY3cgHt1j1n1StVMQVttD19T37dBYTLHlITO5ANtgTAbMemkS3vMsC 13 | AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAFN7wTeOq7xTWYc4ffWvjf/uAYrhUXoRz 14 | 7fQRYaPdqNrmik3Dv+eo9akRr8PQeT8S8wH8nZyQi/V4HOCXOTjqIE9A+XVDysFF 15 | 8Ehs+nKSw6uZAQ5LMVab9GFVAK8CLwRuP03yclEB4thDzykAusZTz7CrfHI96lco 16 | CTJda1YZAEX1Tq4oUsnBzjQ1Y67x9LR/svnLghdDFaN9EvXQOVp1uMDfWfIlD8Pd 17 | ZqH/FC+PIZk8oT/DcgZDfH9JOnnqRpqmuZ3h6sdn66opDRCqD6wXpCwbgFwJAF/h 18 | SOU4bRRVhMsWIoHc76Gc99P+5WQRSIfxU/pi0iqP7V8vRnfynuD0pg== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /kamon-akka-http/src/test/resources/https/rootCA.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDazCCAlOgAwIBAgIUDA+mHhmaAXWoplJ+4vuWX27TNewwDQYJKoZIhvcNAQEL 3 | BQAwRTELMAkGA1UEBhMCSFIxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0xOTA4MTAwODI3NTdaFw0yOTA1 5 | MDkwODI3NTdaMEUxCzAJBgNVBAYTAkhSMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw 6 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB 7 | AQUAA4IBDwAwggEKAoIBAQDDcWKGaVMQgf2NaQCFsKRKSYI2B18gjVAeRDkvVWuf 8 | XiR2ZH4DNMJ7H+FDN8G22ngkO/+3kIQ5cFh2lKgstjQUMu00Zc/xv+SYzL7WpgR6 9 | r1rxMs4LSRvEg6UivLk79trdSME7fz6u/H3L5rR5J6B9ha+BKxn8+bOFZvC/GAM6 10 | 75u+C+7ymcVFEqZIKzPvY4kD+naIIr40hnHf1nwh0ku7wxr+ih460jwSQIM9iVPQ 11 | LTmH1YbxtizBs4Fk3/qhEkiujtZPluQAD20wPkKU7kwMt0tVp2eBEahFkY+erwLS 12 | my8viAT3Py4ACvMWGBTTg0GYr8s8/cpz4+UW3H99kx0rAgMBAAGjUzBRMB0GA1Ud 13 | DgQWBBQbUz5bJSWJ6P8MEGTrMESAgcWSejAfBgNVHSMEGDAWgBQbUz5bJSWJ6P8M 14 | EGTrMESAgcWSejAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAa 15 | ffFvW1xdjQExZSANCOIxPpQL/tqVjzhVUEU4MK9vyZynemqdEXGeWg0P638wXsIc 16 | U3baTmi0IFv+7XdLDSLPB83R6h4m03TBmYY8IfmedqppZVcKd4e1H+bzakMJS3FJ 17 | Xq5/mt80Lb5FYaDw7OgYntdFPKW+wrIE+Hsz/lB0PxGUW2Qqvlt48fFuBbj0xR8n 18 | InCUbr7bUz5K01med27ZrMrWrMfwMuUmxki3ZV6Gw0RYAottTfi5vjjR4j5Xm0nk 19 | 2AkQKVrKuVJvTGZdtXP80s0r7Ckk/inlKq7OyJth9Ff17uyIPcL1pBRhH7gVfc3I 20 | p7S/IIgiwccbfMqVAKxe 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /kamon-akka-http/src/test/scala/kamon/akka/http/ServerFlowWrapperSpec.scala: -------------------------------------------------------------------------------- 1 | package kamon.akka.http 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.model.{HttpEntity, HttpRequest, HttpResponse, StatusCodes} 5 | import akka.stream.ActorMaterializer 6 | import akka.stream.scaladsl.{Flow, Sink, Source} 7 | import kamon.instrumentation.akka.http.ServerFlowWrapper 8 | import org.scalatest.concurrent.ScalaFutures 9 | import org.scalatest.{Matchers, WordSpecLike} 10 | 11 | class ServerFlowWrapperSpec extends WordSpecLike with Matchers with ScalaFutures { 12 | 13 | implicit private val system = ActorSystem("http-client-instrumentation-spec") 14 | implicit private val executor = system.dispatcher 15 | implicit private val materializer = ActorMaterializer() 16 | 17 | private val okReturningFlow = Flow[HttpRequest].map { _ => 18 | HttpResponse(status = StatusCodes.OK, entity = HttpEntity("OK")) 19 | } 20 | 21 | "the server flow wrapper" should { 22 | "keep strict entities strict" in { 23 | val flow = ServerFlowWrapper(okReturningFlow, "localhost", 8080) 24 | val request = HttpRequest() 25 | val response = Source.single(request) 26 | .via(flow) 27 | .runWith(Sink.head) 28 | .futureValue 29 | response.entity should matchPattern { 30 | case HttpEntity.Strict(_, _) => 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /kamon-akka-http/src/test/scala/kamon/testkit/TestNameGenerator.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2013-2016 the kamon project 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 6 | * except in compliance with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the 11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | * either express or implied. See the License for the specific language governing permissions 13 | * and limitations under the License. 14 | * ========================================================================================= 15 | */ 16 | 17 | package kamon.testkit 18 | 19 | import kamon.instrumentation.http.{HttpMessage, HttpOperationNameGenerator} 20 | 21 | class TestNameGenerator extends HttpOperationNameGenerator { 22 | 23 | 24 | 25 | // def serverOperationName(request: HttpRequest): String = { 26 | // val path = request.uri.path.toString() 27 | // // turns "/dummy-path" into "dummy" 28 | // path.substring(1).split("-")(0) 29 | // } 30 | // 31 | // def clientOperationName(request: HttpRequest): String = "client " + request.uri.path.toString() 32 | override def name(request: HttpMessage.Request): Option[String] = Some("test") 33 | } 34 | -------------------------------------------------------------------------------- /kamon-akka-http/src/main/scala/kamon/instrumentation/akka/http/TracingDirectives.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2013-2016 the kamon project 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 6 | * except in compliance with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the 11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | * either express or implied. See the License for the specific language governing permissions 13 | * and limitations under the License. 14 | * ========================================================================================= 15 | */ 16 | package kamon.instrumentation.akka.http 17 | 18 | import akka.http.scaladsl.server.Directive0 19 | import akka.http.scaladsl.server.directives.BasicDirectives 20 | import kamon.Kamon 21 | 22 | 23 | trait TracingDirectives extends BasicDirectives { 24 | 25 | /** 26 | * Assigns a new operation name to the Span representing the processing of the current request and ensures that a 27 | * Sampling Decision is taken in case none has been taken so far. 28 | */ 29 | def operationName(name: String, takeSamplingDecision: Boolean = true): Directive0 = mapRequest { req => 30 | val operationSpan = Kamon.currentSpan() 31 | operationSpan.name(name) 32 | 33 | if(takeSamplingDecision) 34 | operationSpan.takeSamplingDecision() 35 | 36 | req 37 | } 38 | } 39 | 40 | object TracingDirectives extends TracingDirectives 41 | -------------------------------------------------------------------------------- /kamon-akka-http/src/test/resources/https/rootCA.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAw3FihmlTEIH9jWkAhbCkSkmCNgdfII1QHkQ5L1Vrn14kdmR+ 3 | AzTCex/hQzfBttp4JDv/t5CEOXBYdpSoLLY0FDLtNGXP8b/kmMy+1qYEeq9a8TLO 4 | C0kbxIOlIry5O/ba3UjBO38+rvx9y+a0eSegfYWvgSsZ/PmzhWbwvxgDOu+bvgvu 5 | 8pnFRRKmSCsz72OJA/p2iCK+NIZx39Z8IdJLu8Ma/ooeOtI8EkCDPYlT0C05h9WG 6 | 8bYswbOBZN/6oRJIro7WT5bkAA9tMD5ClO5MDLdLVadngRGoRZGPnq8C0psvL4gE 7 | 9z8uAArzFhgU04NBmK/LPP3Kc+PlFtx/fZMdKwIDAQABAoIBAG/vna07344h1TVL 8 | gTgQjlfZuBD3sdzz8oITMulQNB6HjbydG6r8abKY9KxJ39G5WHvwPSpGQ+Sd2py3 9 | 0YYiKLu02zRaZ3mfHO8CvP41AXW+vwhLv8So75VijI7TpgeY/4sjY0CPRTh1dhr1 10 | HEITlxCtI3KIXA8OeGocJiBcQWVb28GWx6A3R1W+CWcgydw4mR2S8YeCnS7y175s 11 | Ni6emuQIb7CZp6sqGK5xXs/iy0RMLRNi6f5IEPH/zn+l1G+XmfR7HAL+CRTkuNDU 12 | dlp30gUuOyntVZgzU5rSrt00vDg7TR92GLsFQscmdT+uIdhpy79yksA6K4e+VEOQ 13 | D+hTSdECgYEA6tjZche6qcCM7vjKTqIEvcpKG4UdWAPH6qNwkn/V/hwqVwXzovej 14 | TJwXvo9oq9MNOC3vBqpYpxa+pgcbfSEab8NtP4eMFZkB6mSDPMaQ+Mh3XfKio7U9 15 | ghjnvLK0ravY0/sPltTZOjv6srY9+wGCD/Ung4SH9AhEmrXaD9P3T2MCgYEA1Qvy 16 | TqW/s/xAC7JP/r3sLucitDfe4VV8KziwKv4rkjz1lbqp+NpJ6SYrn87R6HCjLzUA 17 | rMCruN/JyIaHvcIEBJFeaLkld2BBGEeH4EEGuX1AiC3kUuxjrUbbsjLgq/edHXEi 18 | sN7GaAIeqrcya5CSuXFmpB3Uuuas0K2gZeEsGZkCgYBhC98/gILIZyNWFUU0nUss 19 | So25NZbcqiNQ2N1KDL2XVnhAodr+OysmG1LMkmKErqBF2OVvcbFUytdZsJIxcR6F 20 | lNJucEr5GdNq0sJQuRVrWRvKnNuMnvad7kDE/2weYGcnohXdFHP31pVQiHKwaP0g 21 | LwR3Gqs7srb237MO217VVQKBgByzgFBCGiJoQESTIB3EflYPQ2ieAkO/HXxBJdKU 22 | 7U/FMJycShvBZKWpQ8VCupqi2gkZDd84EapVU7zVCuJwidQHtX1MPBTp/bsEn/SB 23 | LiO9EP2HmTPmrsMAQcau/f+M2zjFLhQ/3uDSMEl1ZrCBCJM9CMPhVPBc9TkjuvEe 24 | ta85AoGBALxexsnE4SX3KREUmw7etEGOmnk2bpgVfCFe1ewDbCnsajAFKU9cizcx 25 | EYVIOEkCuwX+VfHt2Z1yEJrWYtT8SoLwIJiZRNIns2iAtQiifaDOkBRewwNcM6p1 26 | nKzia++0GpKhsj0CZ6zMpGx1/LzT26IceaRLSIHsE58OHG9kKlVz 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /kamon-akka-http/src/test/resources/https/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAqaIRtuBrP4nMsIbSANw9YUWh5zIQrXwDCussvBLP4Lc+G9gI 3 | a/2z+ipsQB9hfgt4WtpUH0Wik6EFi2kC8LUqh/Pm9WKAU3H5arT1+i0d0pPDAcRr 4 | WAlzMiJzd6icpY0tMzoZ7S/stj9v1n2UHffVNFTat48Zhkh470NEXqyeMti3Vhem 5 | ot/spLRR7ytkprxjV8xS85IBMJfXzxcMgdqVs2Jfg+H5sk8ewkMY28hjn/s9Vfrp 6 | saMQ5Kgei9m7Tia2fuJwxSHbhAXVh9MRgXvJVzLsQ/X6Mtge8FjdyAe3WPWfVK1U 7 | xBW20PX1Pft0FhMseUhM7kA22BMBsx6aRLe8ywIDAQABAoIBAGJJosP4soulN3HN 8 | HF8dPX9gDlhcXOd4ZHbuHwR2Tfahlh4iBXc1EBRSgliBFkcnNDxIJtfbzECH2yOU 9 | 2/xGrHcLrnXd9gbjkiXu5ltnytDZhvM+MQhYqWOSLJ9XljQiYd89ugoBa8GJbi60 10 | op7em61vwS78fkidM11G95V3pU5F697JjsKtxh5M0gQwLBOy10/QNcChLKBAHDan 11 | /KpgdpUxE5Z6cLC98/S0Ygn7RWSR0LzpzdyJV+VUAWKt+zZNVBv9QmxWA+eKYAPh 12 | WMyFgAdKSu6pWZRarUuyxrcUiEHefbErCp/OOkAbFGlr9aEEPwCYVOwP5guTA+Y5 13 | 1nYmd7kCgYEA20mh5FuBFF1PwpzIbrsZDgh/OxD4J4WGcXKFbeM01Se+rgKjdvdw 14 | pPGk3xh8K8POSqO/ystj20zu+VZngfM79sEPwWdJPWJ5cACQsqqvKv9FoXvLeBSk 15 | PA+NXEDHrTqzmajqtHSaGEKNjeGrycC3oQAZopk50oC0KsGVF5/+FR8CgYEAxghP 16 | nifQ6Gr4BHbgtCnX9xBnqj2GnxShVloPQSa5+HZ//b4DWE90/5CAu5PJkBbAkUZT 17 | 1I6f4aXohU5eFmeUs+hk10C4J7XlaoWt5Rl8ScxOOMg0YFueAjXvPZZNc/4ppbnw 18 | kKoH3zTPLuVi5GR8ld66r4HN2WLl88Tjm4wQltUCgYAaL5bHgC3P0ry9jp9Yqbr6 19 | NAWNdh9MCOPfFD/euW0Lry1T9jiy8iVfbQO1KGVbjIxL2XYDr3oDLBK1b534pKUa 20 | eD97ZuwWCnZZ65db3ooAZm9YM0I+2qgqC+ljhNDTXNkplkRAvFPSZdAlizdKZlsH 21 | PM3S3t1Kx9e761X0dkSPHQKBgQCB7G/37l10LsH7g9bWvOEw+fVZTrZk5k8XbUy2 22 | zOaUKYK9gg2FwdOb3D1pU4OZYiQC6+YR/WTN0WClHQ5Dmr+H7T9DrfVkMEWMxpmZ 23 | RkgxzrW/MTKTyWf4QVRtzo+QOz8tuLko4DT77xTCysI/3+GRHijS/tGD/wupDBLc 24 | OV+k5QKBgQC+scfAqGuMN1mwPg3/c1u7W/vEA07yIhi2zGA+kpmdjQ/yjGWtaWKi 25 | Jw5E7RP2z5klL821HlAxZQUuXfVGKQ7rEEqBF7BVfYA/qDG3Lxq276N0mjbWr0+G 26 | jeYE4jrsR9UBGZyPF86DbGMRdEWZt1/4AEQTRKyZyhAI79fLkzPG4A== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /kamon-akka-http/src/main/java/kamon/instrumentation/akka/http/HttpExtSingleRequestAdvice.java: -------------------------------------------------------------------------------- 1 | package kamon.instrumentation.akka.http; 2 | 3 | import akka.http.scaladsl.model.HttpRequest; 4 | import akka.http.scaladsl.model.HttpResponse; 5 | import kamon.Kamon; 6 | import kamon.context.Storage; 7 | import kamon.instrumentation.http.HttpClientInstrumentation; 8 | import kamon.instrumentation.http.HttpMessage; 9 | import kamon.trace.Span; 10 | import kanela.agent.libs.net.bytebuddy.asm.Advice; 11 | import scala.concurrent.Future; 12 | import static kamon.instrumentation.akka.http.AkkaHttpInstrumentation.toRequestBuilder; 13 | 14 | public class HttpExtSingleRequestAdvice { 15 | 16 | @Advice.OnMethodEnter 17 | public static void onEnter(@Advice.Argument(value = 0, readOnly = false) HttpRequest request, 18 | @Advice.Local("handler") HttpClientInstrumentation.RequestHandler handler, 19 | @Advice.Local("scope")Storage.Scope scope) { 20 | 21 | final HttpMessage.RequestBuilder requestBuilder = toRequestBuilder(request); 22 | 23 | handler = AkkaHttpClientInstrumentation.httpClientInstrumentation() 24 | .createHandler(requestBuilder, Kamon.currentContext()); 25 | 26 | request = handler.request(); 27 | scope = Kamon.storeContext(Kamon.currentContext().withEntry(Span.Key(), handler.span())); 28 | } 29 | 30 | @Advice.OnMethodExit 31 | public static void onExit(@Advice.Return Future response, 32 | @Advice.Local("handler") HttpClientInstrumentation.RequestHandler handler, 33 | @Advice.Local("scope")Storage.Scope scope) { 34 | 35 | AkkaHttpClientInstrumentation.handleResponse(response, handler); 36 | scope.close(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /kamon-akka-http/src/test/resources/https/chain.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDJDCCAgwCFCBVhkPbLsrPvH5jjO4+u9O6rcRzMA0GCSqGSIb3DQEBCwUAMEUx 3 | CzAJBgNVBAYTAkhSMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl 4 | cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTkwODEwMDgyODU3WhcNMjkwNTA5MDgy 5 | ODU3WjBYMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE 6 | CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMREwDwYDVQQDDAhrYW1vbi5pbzCC 7 | ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKmiEbbgaz+JzLCG0gDcPWFF 8 | oecyEK18AwrrLLwSz+C3PhvYCGv9s/oqbEAfYX4LeFraVB9FopOhBYtpAvC1Kofz 9 | 5vVigFNx+Wq09fotHdKTwwHEa1gJczIic3eonKWNLTM6Ge0v7LY/b9Z9lB331TRU 10 | 2rePGYZIeO9DRF6snjLYt1YXpqLf7KS0Ue8rZKa8Y1fMUvOSATCX188XDIHalbNi 11 | X4Ph+bJPHsJDGNvIY5/7PVX66bGjEOSoHovZu04mtn7icMUh24QF1YfTEYF7yVcy 12 | 7EP1+jLYHvBY3cgHt1j1n1StVMQVttD19T37dBYTLHlITO5ANtgTAbMemkS3vMsC 13 | AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAFN7wTeOq7xTWYc4ffWvjf/uAYrhUXoRz 14 | 7fQRYaPdqNrmik3Dv+eo9akRr8PQeT8S8wH8nZyQi/V4HOCXOTjqIE9A+XVDysFF 15 | 8Ehs+nKSw6uZAQ5LMVab9GFVAK8CLwRuP03yclEB4thDzykAusZTz7CrfHI96lco 16 | CTJda1YZAEX1Tq4oUsnBzjQ1Y67x9LR/svnLghdDFaN9EvXQOVp1uMDfWfIlD8Pd 17 | ZqH/FC+PIZk8oT/DcgZDfH9JOnnqRpqmuZ3h6sdn66opDRCqD6wXpCwbgFwJAF/h 18 | SOU4bRRVhMsWIoHc76Gc99P+5WQRSIfxU/pi0iqP7V8vRnfynuD0pg== 19 | -----END CERTIFICATE----- 20 | -----BEGIN CERTIFICATE----- 21 | MIIDazCCAlOgAwIBAgIUDA+mHhmaAXWoplJ+4vuWX27TNewwDQYJKoZIhvcNAQEL 22 | BQAwRTELMAkGA1UEBhMCSFIxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 23 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0xOTA4MTAwODI3NTdaFw0yOTA1 24 | MDkwODI3NTdaMEUxCzAJBgNVBAYTAkhSMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw 25 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB 26 | AQUAA4IBDwAwggEKAoIBAQDDcWKGaVMQgf2NaQCFsKRKSYI2B18gjVAeRDkvVWuf 27 | XiR2ZH4DNMJ7H+FDN8G22ngkO/+3kIQ5cFh2lKgstjQUMu00Zc/xv+SYzL7WpgR6 28 | r1rxMs4LSRvEg6UivLk79trdSME7fz6u/H3L5rR5J6B9ha+BKxn8+bOFZvC/GAM6 29 | 75u+C+7ymcVFEqZIKzPvY4kD+naIIr40hnHf1nwh0ku7wxr+ih460jwSQIM9iVPQ 30 | LTmH1YbxtizBs4Fk3/qhEkiujtZPluQAD20wPkKU7kwMt0tVp2eBEahFkY+erwLS 31 | my8viAT3Py4ACvMWGBTTg0GYr8s8/cpz4+UW3H99kx0rAgMBAAGjUzBRMB0GA1Ud 32 | DgQWBBQbUz5bJSWJ6P8MEGTrMESAgcWSejAfBgNVHSMEGDAWgBQbUz5bJSWJ6P8M 33 | EGTrMESAgcWSejAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAa 34 | ffFvW1xdjQExZSANCOIxPpQL/tqVjzhVUEU4MK9vyZynemqdEXGeWg0P638wXsIc 35 | U3baTmi0IFv+7XdLDSLPB83R6h4m03TBmYY8IfmedqppZVcKd4e1H+bzakMJS3FJ 36 | Xq5/mt80Lb5FYaDw7OgYntdFPKW+wrIE+Hsz/lB0PxGUW2Qqvlt48fFuBbj0xR8n 37 | InCUbr7bUz5K01med27ZrMrWrMfwMuUmxki3ZV6Gw0RYAottTfi5vjjR4j5Xm0nk 38 | 2AkQKVrKuVJvTGZdtXP80s0r7Ckk/inlKq7OyJth9Ff17uyIPcL1pBRhH7gVfc3I 39 | p7S/IIgiwccbfMqVAKxe 40 | -----END CERTIFICATE----- 41 | -------------------------------------------------------------------------------- /kamon-akka-http/src/main/scala/kamon/instrumentation/akka/http/AkkaHttpClientInstrumentation.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2013-2016 the kamon project 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 6 | * except in compliance with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the 11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | * either express or implied. See the License for the specific language governing permissions 13 | * and limitations under the License. 14 | * ========================================================================================= 15 | */ 16 | 17 | package kamon.instrumentation.akka.http 18 | 19 | import akka.http.scaladsl.model.{HttpRequest, HttpResponse} 20 | import kamon.Kamon 21 | import kamon.instrumentation.http.HttpClientInstrumentation 22 | import kamon.instrumentation.akka.http.AkkaHttpInstrumentation.toResponse 23 | import kamon.instrumentation.http.HttpClientInstrumentation.RequestHandler 24 | import kamon.util.CallingThreadExecutionContext 25 | import kanela.agent.api.instrumentation.InstrumentationBuilder 26 | 27 | import scala.concurrent.Future 28 | import scala.util.{Failure, Success} 29 | 30 | class AkkaHttpClientInstrumentation extends InstrumentationBuilder { 31 | 32 | /** 33 | * Simply modifies the requests as they are submitted. This does not cover connection pooling, just requests sent 34 | * via the Http.singleRequest mechanism. 35 | */ 36 | onType("akka.http.scaladsl.HttpExt") 37 | .advise(method("singleRequestImpl"), classOf[HttpExtSingleRequestAdvice]) 38 | } 39 | 40 | object AkkaHttpClientInstrumentation { 41 | 42 | @volatile var httpClientInstrumentation: HttpClientInstrumentation = rebuildHttpClientInstrumentation 43 | 44 | private[http] def rebuildHttpClientInstrumentation(): HttpClientInstrumentation = { 45 | val httpClientConfig = Kamon.config().getConfig("kamon.instrumentation.akka.http.client") 46 | httpClientInstrumentation = HttpClientInstrumentation.from(httpClientConfig, "akka.http.client") 47 | httpClientInstrumentation 48 | } 49 | 50 | def handleResponse(responseFuture: Future[HttpResponse], handler: RequestHandler[HttpRequest]): Future[HttpResponse] = { 51 | responseFuture.onComplete { 52 | case Success(response) => handler.processResponse(toResponse(response)) 53 | case Failure(t) => handler.span.fail(t).finish() 54 | }(CallingThreadExecutionContext) 55 | 56 | responseFuture 57 | } 58 | } -------------------------------------------------------------------------------- /kamon-akka-http/src/main/scala/kamon/instrumentation/akka/http/AkkaHttpInstrumentation.scala: -------------------------------------------------------------------------------- 1 | package kamon.instrumentation.akka.http 2 | 3 | import akka.http.scaladsl.model.{HttpHeader, HttpRequest, HttpResponse} 4 | import akka.http.scaladsl.model.headers.RawHeader 5 | import kamon.Kamon 6 | import kamon.instrumentation.http.HttpMessage 7 | 8 | import scala.collection.immutable 9 | 10 | object AkkaHttpInstrumentation { 11 | 12 | Kamon.onReconfigure(_ => AkkaHttpClientInstrumentation.rebuildHttpClientInstrumentation(): Unit) 13 | 14 | def toRequest(httpRequest: HttpRequest): HttpMessage.Request = new RequestReader { 15 | val request = httpRequest 16 | } 17 | 18 | def toResponse(httpResponse: HttpResponse): HttpMessage.Response = new HttpMessage.Response { 19 | override val statusCode: Int = httpResponse.status.intValue() 20 | } 21 | 22 | def toRequestBuilder(httpRequest: HttpRequest): HttpMessage.RequestBuilder[HttpRequest] = 23 | new RequestReader with HttpMessage.RequestBuilder[HttpRequest] { 24 | private var _extraHeaders = List.empty[RawHeader] 25 | val request = httpRequest 26 | 27 | override def write(header: String, value: String): Unit = 28 | _extraHeaders = RawHeader(header, value) :: _extraHeaders 29 | 30 | override def build(): HttpRequest = 31 | request.withHeaders(request.headers ++ _extraHeaders) 32 | } 33 | 34 | def toResponseBuilder(response: HttpResponse): HttpMessage.ResponseBuilder[HttpResponse] = new HttpMessage.ResponseBuilder[HttpResponse] { 35 | private var _headers = response.headers 36 | 37 | override def statusCode: Int = 38 | response.status.intValue() 39 | 40 | override def write(header: String, value: String): Unit = 41 | _headers = RawHeader(header, value) +: _headers 42 | 43 | override def build(): HttpResponse = 44 | response.withHeaders(_headers) 45 | } 46 | 47 | /** 48 | * Bundles together the read parts of the HTTP Request mapping 49 | */ 50 | private trait RequestReader extends HttpMessage.Request { 51 | def request: HttpRequest 52 | 53 | override def url: String = 54 | request.uri.toString() 55 | 56 | override def path: String = 57 | request.uri.path.toString() 58 | 59 | override def method: String = 60 | request.method.value 61 | 62 | override def host: String = 63 | request.uri.authority.host.address() 64 | 65 | override def port: Int = 66 | request.uri.authority.port 67 | 68 | override def read(header: String): Option[String] = { 69 | val headerValue = request.getHeader(header) 70 | if(headerValue.isPresent) 71 | Some(headerValue.get().value()) 72 | else None 73 | } 74 | 75 | override def readAll(): Map[String, String] = { 76 | val builder = Map.newBuilder[String, String] 77 | request.headers.foreach(h => builder += (h.name() -> h.value())) 78 | builder.result() 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /kamon-akka-http/src/test/scala/kamon/akka/http/AkkaHttpServerMetricsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2013-2016 the kamon project 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 6 | * except in compliance with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the 11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | * either express or implied. See the License for the specific language governing permissions 13 | * and limitations under the License. 14 | * ========================================================================================= 15 | */ 16 | 17 | package kamon.akka.http 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.Http 21 | import akka.http.scaladsl.model.{HttpRequest, HttpResponse} 22 | import akka.http.scaladsl.settings.ClientConnectionSettings 23 | import akka.stream.ActorMaterializer 24 | import akka.stream.scaladsl.{Sink, Source} 25 | import kamon.instrumentation.http.HttpServerMetrics 26 | import kamon.testkit._ 27 | import org.scalatest.concurrent.Eventually 28 | import org.scalatest.{BeforeAndAfterAll, Matchers, OptionValues, WordSpecLike} 29 | 30 | import scala.concurrent.Future 31 | import scala.concurrent.duration._ 32 | 33 | class AkkaHttpServerMetricsSpec extends WordSpecLike with Matchers with BeforeAndAfterAll with InstrumentInspection.Syntax 34 | with Reconfigure with TestWebServer with Eventually with OptionValues { 35 | 36 | import TestWebServer.Endpoints._ 37 | 38 | implicit private val system = ActorSystem("http-server-metrics-instrumentation-spec") 39 | implicit private val executor = system.dispatcher 40 | implicit private val materializer = ActorMaterializer() 41 | 42 | val port = 8083 43 | val interface = "127.0.0.1" 44 | val timeoutTest: FiniteDuration = 5 second 45 | val webServer = startServer(interface, port) 46 | 47 | "the Akka HTTP server instrumentation" should { 48 | "track the number of open connections and active requests on the Server side" in { 49 | val httpServerMetrics = HttpServerMetrics.of("akka.http.server", interface, port) 50 | 51 | for(_ <- 1 to 8) yield { 52 | sendRequest(HttpRequest(uri = s"http://$interface:$port/$waitTen")) 53 | } 54 | 55 | eventually(timeout(10 seconds)) { 56 | httpServerMetrics.openConnections.distribution().max shouldBe(8) 57 | httpServerMetrics.activeRequests.distribution().max shouldBe(8) 58 | } 59 | 60 | eventually(timeout(20 seconds)) { 61 | httpServerMetrics.openConnections.distribution().max shouldBe(0) 62 | httpServerMetrics.activeRequests.distribution().max shouldBe(0) 63 | } 64 | } 65 | } 66 | 67 | def sendRequest(request: HttpRequest): Future[HttpResponse] = { 68 | val connectionSettings = ClientConnectionSettings(system).withIdleTimeout(1 second) 69 | Source.single(request) 70 | .via(Http().outgoingConnection(interface, port, settings = connectionSettings)) 71 | .map{r => 72 | r.discardEntityBytes() 73 | r 74 | } 75 | .runWith(Sink.head) 76 | } 77 | 78 | override protected def afterAll(): Unit = { 79 | webServer.shutdown() 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /kamon-akka-http/src/test/scala/kamon/akka/http/FastFutureInstrumentationSpec.scala: -------------------------------------------------------------------------------- 1 | package kamon.akka.http 2 | 3 | import java.util.concurrent.CountDownLatch 4 | 5 | import akka.http.scaladsl.util.FastFuture 6 | import kamon.instrumentation.context.HasContext 7 | import org.scalatest.{Matchers, WordSpec} 8 | 9 | import scala.concurrent.duration._ 10 | import scala.concurrent.ExecutionContext.Implicits.global 11 | import scala.concurrent.{Await, Future, Promise} 12 | import kamon.instrumentation.futures.scala.ScalaFutureInstrumentation.trace 13 | import akka.http.scaladsl.util.FastFuture.EnhancedFuture 14 | import kamon.Kamon 15 | import kamon.context.Context 16 | 17 | import scala.util.Try 18 | 19 | class FastFutureInstrumentationSpec extends WordSpec with Matchers { 20 | 21 | "the FastFuture instrumentation" should { 22 | "keep the Context captured by the Future from which it was created" when { 23 | "calling .map/.flatMap/.onComplete and the original Future has not completed yet" in { 24 | val completeSignal = new CountDownLatch(1) 25 | val future = trace("async-operation") { 26 | Future { 27 | completeSignal.await() 28 | "Hello World" 29 | } 30 | } 31 | 32 | val onCompleteFuture = Promise[Context] 33 | val fastFutures = Seq( 34 | future.fast.map(_ => Kamon.currentContext()), 35 | future.fast.flatMap(_ => Future(Kamon.currentContext())), 36 | future.fast.map(_ => "").flatMap(_ => Future(Kamon.currentContext())), 37 | future.fast.map(_ => "").map(_ => Kamon.currentContext()), 38 | future.fast.map(_ => { val c = Kamon.currentContext(); onCompleteFuture.complete(Try(c)); c }), 39 | onCompleteFuture.future 40 | ) 41 | 42 | // When the future is finished, the Context stored on it should have the 43 | // Span for the async-operation above, but the current Thread should be clean. 44 | Kamon.currentContext() shouldBe empty 45 | completeSignal.countDown() 46 | Await.ready(future, 10 seconds) 47 | val fastFutureContexts = Await.result(FastFuture.sequence(fastFutures), 10 seconds) 48 | val futureContext = future.value.get.asInstanceOf[HasContext].context 49 | 50 | 51 | fastFutureContexts.foreach(context => context shouldBe futureContext) 52 | } 53 | 54 | "calling .map/.flatMap/.onComplete and the original Future has already completed" in { 55 | val completeSignal = new CountDownLatch(1) 56 | val future = trace("async-operation") { 57 | Future { 58 | completeSignal.await() 59 | "Hello World" 60 | } 61 | } 62 | 63 | // When the future is finished, the Context stored on it should have the 64 | // Span for the async-operation above, but the current Thread should be clean. 65 | Kamon.currentContext() shouldBe empty 66 | completeSignal.countDown() 67 | Await.ready(future, 10 seconds) 68 | val futureContext = future.value.get.asInstanceOf[HasContext].context 69 | 70 | val onCompleteFuture = Promise[Context] 71 | val fastFutures = Seq( 72 | future.fast.map(_ => Kamon.currentContext()), 73 | future.fast.flatMap(_ => Future(Kamon.currentContext())), 74 | future.fast.map(_ => "").flatMap(_ => Future(Kamon.currentContext())), 75 | future.fast.map(_ => "").map(_ => Kamon.currentContext()), 76 | future.fast.map(_ => { val c = Kamon.currentContext(); onCompleteFuture.complete(Try(c)); c }), 77 | onCompleteFuture.future 78 | ) 79 | 80 | val fastFutureContexts = Await.result(FastFuture.sequence(fastFutures), 10 seconds) 81 | fastFutureContexts.foreach(context => context shouldBe futureContext) 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /kamon-akka-http/src/test/scala/kamon/akka/http/AkkaHttpClientTracingSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2013-2016 the kamon project 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 6 | * except in compliance with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the 11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | * either express or implied. See the License for the specific language governing permissions 13 | * and limitations under the License. 14 | * ========================================================================================= 15 | */ 16 | 17 | package kamon.akka.http 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.Http 21 | import akka.http.scaladsl.model.headers.RawHeader 22 | import akka.http.scaladsl.model.HttpRequest 23 | import akka.stream.ActorMaterializer 24 | import kamon.Kamon 25 | import kamon.testkit._ 26 | import kamon.trace.Span 27 | import kamon.tag.Lookups.{plain, plainLong, plainBoolean} 28 | import org.scalatest.concurrent.Eventually 29 | import org.scalatest.{BeforeAndAfterAll, Matchers, OptionValues, WordSpecLike} 30 | import org.json4s._ 31 | import org.json4s.native.JsonMethods._ 32 | 33 | import scala.concurrent.duration._ 34 | 35 | class AkkaHttpClientTracingSpec extends WordSpecLike with Matchers with BeforeAndAfterAll with MetricInspection.Syntax 36 | with Reconfigure with TestWebServer with Eventually with OptionValues with TestSpanReporter { 37 | 38 | import TestWebServer.Endpoints._ 39 | 40 | implicit private val system = ActorSystem("http-client-instrumentation-spec") 41 | implicit private val executor = system.dispatcher 42 | implicit private val materializer = ActorMaterializer() 43 | 44 | val timeoutTest: FiniteDuration = 5 second 45 | val interface = "127.0.0.1" 46 | val port = 8080 47 | val webServer = startServer(interface, port) 48 | 49 | "the Akka HTTP client instrumentation" should { 50 | "create a client Span when using the request level API - Http().singleRequest(...)" in { 51 | val target = s"http://$interface:$port/$dummyPathOk" 52 | Http().singleRequest(HttpRequest(uri = target)).map(_.discardEntityBytes()) 53 | 54 | eventually(timeout(10 seconds)) { 55 | val span = testSpanReporter.nextSpan().value 56 | span.operationName shouldBe "GET" 57 | span.tags.get(plain("http.url")) shouldBe target 58 | span.metricTags.get(plain("component")) shouldBe "akka.http.client" 59 | span.metricTags.get(plain("http.method")) shouldBe "GET" 60 | } 61 | } 62 | 63 | "create a client Span when using the request level API - Http().singleRequest(...) from Java" in { 64 | val target = s"http://$interface:$port/$dummyPathOk" 65 | 66 | val http = akka.http.javadsl.Http.get(system) 67 | http.singleRequest(HttpRequest(uri = target)) 68 | 69 | eventually(timeout(10 seconds)) { 70 | val span = testSpanReporter.nextSpan().value 71 | span.operationName shouldBe "GET" 72 | span.tags.get(plain("http.url")) shouldBe target 73 | span.metricTags.get(plain("component")) shouldBe "akka.http.client" 74 | span.metricTags.get(plain("http.method")) shouldBe "GET" 75 | } 76 | } 77 | 78 | "serialize the current context into HTTP Headers" in { 79 | val target = s"http://$interface:$port/$replyWithHeaders" 80 | val tagKey = "custom.message" 81 | val tagValue = "Hello World :D" 82 | 83 | val response = Kamon.runWithContextTag(tagKey, tagValue) { 84 | Http().singleRequest(HttpRequest(uri = target, headers = List(RawHeader("X-Foo", "bar")))) 85 | }.flatMap(r => r.entity.toStrict(timeoutTest)) 86 | 87 | eventually(timeout(10 seconds)) { 88 | val httpResponse = response.value.value.get 89 | val headersMap = parse(httpResponse.data.utf8String).extract[Map[String, String]] 90 | 91 | headersMap.keys.toList should contain allOf( 92 | "context-tags", 93 | "X-Foo", 94 | "X-B3-TraceId", 95 | "X-B3-SpanId", 96 | "X-B3-Sampled" 97 | ) 98 | 99 | headersMap.get("context-tags").value shouldBe "custom.message=Hello World :D;upstream.name=kamon-application;" 100 | } 101 | } 102 | 103 | "mark Spans as errors if the client request failed" in { 104 | val target = s"http://$interface:$port/$dummyPathError" 105 | Http().singleRequest(HttpRequest(uri = target)).map(_.discardEntityBytes()) 106 | 107 | eventually(timeout(10 seconds)) { 108 | val span = testSpanReporter().nextSpan().value 109 | span.operationName shouldBe "GET" 110 | span.tags.get(plain("http.url")) shouldBe target 111 | span.metricTags.get(plain("component")) shouldBe "akka.http.client" 112 | span.metricTags.get(plain("http.method")) shouldBe "GET" 113 | span.metricTags.get(plainBoolean("error")) shouldBe true 114 | span.metricTags.get(plainLong("http.status_code")) shouldBe 500 115 | span.hasError shouldBe true 116 | } 117 | } 118 | } 119 | 120 | override protected def afterAll(): Unit = { 121 | webServer.shutdown() 122 | } 123 | } 124 | 125 | -------------------------------------------------------------------------------- /kamon-akka-http/src/test/resources/application.conf: -------------------------------------------------------------------------------- 1 | kamon { 2 | trace.sampler = "always" 3 | } 4 | 5 | akka.http.server.preview.enable-http2 = on 6 | 7 | 8 | 9 | kamon.instrumentation.akka.http { 10 | 11 | # Settings to control the HTTP Server instrumentation. 12 | # 13 | # IMPORTANT: Besides the "initial-operation-name" and "unhandled-operation-name" settings, the entire configuration of 14 | # the HTTP Server Instrumentation is based on the constructs provided by the Kamon Instrumentation Common library 15 | # which will always fallback to the settings found under the "kamon.instrumentation.http-server.default" path. The 16 | # default settings have been included here to make them easy to find and understand in the context of this project and 17 | # commented out so that any changes to the default settings will actually have effect. 18 | # 19 | server { 20 | 21 | # 22 | # Configuration for HTTP context propagation. 23 | # 24 | propagation { 25 | 26 | # Enables or disables HTTP context propagation on this HTTP server instrumentation. Please note that if 27 | # propagation is disabled then some distributed tracing features will not be work as expected (e.g. Spans can 28 | # be created and reported but will not be linked across boundaries nor take trace identifiers from tags). 29 | #enabled = yes 30 | 31 | # HTTP propagation channel to b used by this instrumentation. Take a look at the kamon.propagation.http.default 32 | # configuration for more details on how to configure the detault HTTP context propagation. 33 | #channel = "default" 34 | } 35 | 36 | 37 | # 38 | # Configuration for HTTP server metrics collection. 39 | # 40 | metrics { 41 | 42 | # Enables collection of HTTP server metrics. When enabled the following metrics will be collected, assuming 43 | # that the instrumentation is fully compliant: 44 | # 45 | # - http.server.requets 46 | # - http.server.request.active 47 | # - http.server.request.size 48 | # - http.server.response.size 49 | # - http.server.connection.lifetime 50 | # - http.server.connection.usage 51 | # - http.server.connection.open 52 | # 53 | # All metrics have at least three tags: component, interface and port. Additionally, the http.server.requests 54 | # metric will also have a status_code tag with the status code group (1xx, 2xx and so on). 55 | # 56 | #enabled = yes 57 | } 58 | 59 | 60 | # 61 | # Configuration for HTTP request tracing. 62 | # 63 | tracing { 64 | 65 | # Enables HTTP request tracing. When enabled the instrumentation will create Spans for incoming requests 66 | # and finish them when the response is sent back to the clients. 67 | #enabled = yes 68 | 69 | # Select a context tag that provides a preferred trace identifier. The preferred trace identifier will be used 70 | # only if all these conditions are met: 71 | # - the context tag is present. 72 | # - there is no parent Span on the incoming context (i.e. this is the first service on the trace). 73 | # - the identifier is valid in accordance to the identity provider. 74 | #preferred-trace-id-tag = "none" 75 | 76 | # Enables collection of span metrics using the `span.processing-time` metric. 77 | #span-metrics = on 78 | 79 | # Select which tags should be included as span and span metric tags. The possible options are: 80 | # - span: the tag is added as a Span tag (i.e. using span.tag(...)) 81 | # - metric: the tag is added a a Span metric tag (i.e. using span.tagMetric(...)) 82 | # - off: the tag is not used. 83 | # 84 | tags { 85 | 86 | # Use the http.url tag. 87 | #url = span 88 | 89 | # Use the http.method tag. 90 | #method = metric 91 | 92 | # Use the http.status_code tag. 93 | #status-code = metric 94 | 95 | # Copy tags from the context into the Spans with the specified purpouse. For example, to copy a customer_type 96 | # tag from the context into the HTTP Server Span created by the instrumentation, the following configuration 97 | # should be added: 98 | # 99 | # from-context { 100 | # customer_type = span 101 | # } 102 | # 103 | from-context { 104 | 105 | } 106 | } 107 | 108 | # Controls writing trace and span identifiers to HTTP response headers sent by the instrumented servers. The 109 | # configuration can be set to either "none" to disable writing the identifiers on the response headers or to 110 | # the header name to be used when writing the identifiers. 111 | response-headers { 112 | 113 | # HTTP response header name for the trace identifier, or "none" to disable it. 114 | #trace-id = "trace-id" 115 | 116 | # HTTP response header name for the server span identifier, or "none" to disable it. 117 | #span-id = none 118 | } 119 | 120 | # Custom mappings between routes and operation names. 121 | operations { 122 | 123 | # The default operation name to be used when creating Spans to handle the HTTP server requests. In most 124 | # cases it is not possible to define an operation name right at the moment of starting the HTTP server Span 125 | # and in those cases, this operation name will be initially assigned to the Span. Instrumentation authors 126 | # should do their best effort to provide a suitable operation name or make use of the "mappings" facilities. 127 | #default = "http.server.request" 128 | 129 | # Provides custom mappings from HTTP paths into operation names. Meant to be used in cases where the bytecode 130 | # instrumentation is not able to provide a sensible operation name that is free of high cardinality values. 131 | # For example, with the following configuration: 132 | # mappings { 133 | # "/organization/*/user/*/profile" = "/organization/:orgID/user/:userID/profile" 134 | # "/events/*/rsvps" = "EventRSVPs" 135 | # } 136 | # 137 | # Requests to "/organization/3651/user/39652/profile" and "/organization/22234/user/54543/profile" will have 138 | # the same operation name "/organization/:orgID/user/:userID/profile". 139 | # 140 | # Similarly, requests to "/events/aaa-bb-ccc/rsvps" and "/events/1234/rsvps" will have the same operation 141 | # name "EventRSVPs". 142 | # 143 | # The patterns are expressed as globs and the operation names are free form. 144 | # 145 | mappings { 146 | "/name-will-be-changed" = "named-via-config" 147 | } 148 | } 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /kamon-akka-http/src/main/scala/kamon/instrumentation/akka/http/ServerFlowWrapper.scala: -------------------------------------------------------------------------------- 1 | package kamon.instrumentation.akka.http 2 | 3 | import java.time.Duration 4 | import java.util.concurrent.atomic.AtomicLong 5 | 6 | import akka.NotUsed 7 | import akka.http.scaladsl.model.{HttpEntity, HttpRequest, HttpResponse} 8 | import akka.stream.scaladsl.{BidiFlow, Flow, Keep} 9 | import akka.stream.stage.{GraphStage, GraphStageLogic, InHandler, OutHandler} 10 | import akka.stream.{Attributes, BidiShape, Inlet, Outlet} 11 | import akka.util.ByteString 12 | import kamon.Kamon 13 | import kamon.instrumentation.http.HttpServerInstrumentation.RequestHandler 14 | import kamon.instrumentation.http.HttpServerInstrumentation 15 | import kamon.util.CallingThreadExecutionContext 16 | import kamon.instrumentation.akka.http.AkkaHttpInstrumentation.{toRequest, toResponseBuilder} 17 | 18 | import scala.collection.concurrent.TrieMap 19 | import scala.collection.mutable 20 | import scala.util.{Failure, Success} 21 | 22 | /** 23 | * Wraps a {@code Flow[HttpRequest,HttpResponse]} with the necessary infrastructure to provide HTTP Server metrics, 24 | * tracing and Context propagation. 25 | * 26 | * Credits to @jypma. 27 | */ 28 | object ServerFlowWrapper { 29 | 30 | // Since we reuse the same instrumentation to track Play, we are allowing for the component tag to be changed 31 | // temporarily. 32 | private val _serverInstrumentations = TrieMap.empty[Int, HttpServerInstrumentation] 33 | private val _defaultOperationNames = TrieMap.empty[Int, String] 34 | private val _defaultSettings = Settings("akka.http.server", "kamon.instrumentation.akka.http.server") 35 | @volatile private var _wrapperSettings = _defaultSettings 36 | 37 | def apply(flow: Flow[HttpRequest, HttpResponse, NotUsed], interface: String, port: Int): Flow[HttpRequest, HttpResponse, NotUsed] = 38 | BidiFlow.fromGraph(wrapStage(_wrapperSettings, interface, port)).join(flow) 39 | 40 | def wrapStage(settings: Settings, interface: String, port: Int) = new GraphStage[BidiShape[HttpRequest, HttpRequest, HttpResponse, HttpResponse]] { 41 | val httpServerConfig = Kamon.config().getConfig(settings.configPath) 42 | val httpServerInstrumentation = HttpServerInstrumentation.from(httpServerConfig, settings.component, interface, port) 43 | val requestIn = Inlet.create[HttpRequest]("request.in") 44 | val requestOut = Outlet.create[HttpRequest]("request.out") 45 | val responseIn = Inlet.create[HttpResponse]("response.in") 46 | val responseOut = Outlet.create[HttpResponse]("response.out") 47 | 48 | override val shape = BidiShape(requestIn, requestOut, responseIn, responseOut) 49 | 50 | override def createLogic(inheritedAttributes: Attributes) = new GraphStageLogic(shape) { 51 | 52 | // There might be more than one outstanding request when HTTP pipelining is enabled but according to the Akka HTTP 53 | // documentation, it is required by the applications to generate responses for all requests and to generate them 54 | // in the appropriate order, so we can simply queue and dequeue the RequestHandlers as the requests flow through 55 | // the Stage. 56 | // 57 | // More info: https://doc.akka.io/docs/akka-http/current/server-side/low-level-api.html#request-response-cycle 58 | private val _pendingRequests = mutable.Queue.empty[RequestHandler] 59 | private val _createdAt = Kamon.clock().instant() 60 | private var _completedRequests = 0 61 | 62 | setHandler(requestIn, new InHandler { 63 | override def onPush(): Unit = { 64 | val request = grab(requestIn) 65 | val requestHandler = httpServerInstrumentation.createHandler(toRequest(request), deferSamplingDecision = true) 66 | .requestReceived() 67 | 68 | val defaultOperationName = httpServerInstrumentation.settings.defaultOperationName 69 | val requestSpan = requestHandler.span 70 | 71 | // Automatic Operation name changes will only be allowed if the initial name HTTP Server didn't assign a 72 | // user-defined name to the operation (that would be, either via an Operation Name Generator or a custom 73 | // operation mapping). 74 | val allowAutomaticChanges = requestSpan.operationName() == defaultOperationName 75 | 76 | _pendingRequests.enqueue(requestHandler) 77 | 78 | // The only reason why it's safe to leave the Thread dirty is because the Actor 79 | // instrumentation will cleanup afterwards. 80 | Kamon.storeContext(requestHandler.context.withEntry( 81 | LastAutomaticOperationNameEdit.Key, Option(LastAutomaticOperationNameEdit(requestSpan.operationName(), allowAutomaticChanges)) 82 | )) 83 | 84 | push(requestOut, request) 85 | } 86 | 87 | override def onUpstreamFinish(): Unit = 88 | complete(requestOut) 89 | }) 90 | 91 | setHandler(requestOut, new OutHandler { 92 | override def onPull(): Unit = 93 | pull(requestIn) 94 | 95 | override def onDownstreamFinish(): Unit = 96 | cancel(requestIn) 97 | }) 98 | 99 | setHandler(responseIn, new InHandler { 100 | override def onPush(): Unit = { 101 | val response = grab(responseIn) 102 | val requestHandler = _pendingRequests.dequeue() 103 | val requestSpan = requestHandler.span 104 | val responseWithContext = requestHandler.buildResponse(toResponseBuilder(response), requestHandler.context) 105 | 106 | if(response.status.intValue() == 404 && requestSpan.operationName() == httpServerInstrumentation.settings.defaultOperationName) { 107 | 108 | // It might happen that if no route was able to handle the request or no directive that would force taking 109 | // a sampling decision was run, the request would still not have any sampling decision so we both set the 110 | // request as unhandled and take a sampling decision for that operation here. 111 | requestSpan 112 | .name(httpServerInstrumentation.settings.unhandledOperationName) 113 | .takeSamplingDecision() 114 | } 115 | 116 | val entity = if(responseWithContext.entity.isKnownEmpty()) { 117 | requestHandler.responseSent(0L) 118 | responseWithContext.entity 119 | } else { 120 | 121 | requestSpan.mark("http.response.ready") 122 | 123 | responseWithContext.entity match { 124 | case strict@HttpEntity.Strict(_, bs) => 125 | requestHandler.responseSent(bs.size) 126 | strict 127 | case _ => 128 | val responseSizeCounter = new AtomicLong(0L) 129 | responseWithContext.entity.transformDataBytes( 130 | Flow[ByteString] 131 | .watchTermination()(Keep.right) 132 | .wireTap(bs => responseSizeCounter.addAndGet(bs.size)) 133 | .mapMaterializedValue { f => 134 | f.andThen { 135 | case Success(_) => 136 | requestHandler.responseSent(responseSizeCounter.get()) 137 | case Failure(e) => 138 | requestSpan.fail("Response entity stream failed", e) 139 | requestHandler.responseSent(responseSizeCounter.get()) 140 | 141 | }(CallingThreadExecutionContext) 142 | } 143 | ) 144 | } 145 | } 146 | 147 | _completedRequests += 1 148 | push(responseOut, responseWithContext.withEntity(entity)) 149 | } 150 | 151 | override def onUpstreamFinish(): Unit = 152 | completeStage() 153 | }) 154 | 155 | setHandler(responseOut, new OutHandler { 156 | override def onPull(): Unit = 157 | pull(responseIn) 158 | 159 | override def onDownstreamFinish(): Unit = 160 | cancel(responseIn) 161 | }) 162 | 163 | override def preStart(): Unit = 164 | httpServerInstrumentation.connectionOpened() 165 | 166 | override def postStop(): Unit = { 167 | val connectionLifetime = Duration.between(_createdAt, Kamon.clock().instant()) 168 | httpServerInstrumentation.connectionClosed(connectionLifetime, _completedRequests) 169 | } 170 | } 171 | } 172 | 173 | def changeSettings(component: String, configPath: String): Unit = 174 | _wrapperSettings = Settings(component, configPath) 175 | 176 | def resetSettings(): Unit = 177 | _wrapperSettings = _defaultSettings 178 | 179 | def defaultOperationName(listenPort: Int): String = 180 | _defaultOperationNames.getOrElseUpdate(listenPort, { 181 | _serverInstrumentations.get(listenPort).map(_.settings.defaultOperationName).getOrElse("http.server.request") 182 | }) 183 | 184 | case class Settings(component: String, configPath: String) 185 | } 186 | -------------------------------------------------------------------------------- /kamon-akka-http/src/test/scala/kamon/testkit/TestWebServer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2013-2016 the kamon project 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 6 | * except in compliance with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the 11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | * either express or implied. See the License for the specific language governing permissions 13 | * and limitations under the License. 14 | * ========================================================================================= 15 | */ 16 | 17 | package kamon.testkit 18 | 19 | import java.security.cert.{Certificate, CertificateFactory} 20 | import java.security.{KeyStore, SecureRandom} 21 | 22 | import akka.actor.ActorSystem 23 | import akka.http.scaladsl.{Http, HttpsConnectionContext, UseHttp2} 24 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpResponse} 25 | import de.heikoseeberger.akkahttpjson4s.Json4sSupport 26 | import akka.http.scaladsl.model.StatusCodes.{BadRequest, InternalServerError, OK} 27 | import akka.http.scaladsl.model.headers.{Connection, RawHeader} 28 | import akka.http.scaladsl.server.Directives._ 29 | import akka.http.scaladsl.server.{RequestContext, Route} 30 | import akka.stream.ActorMaterializer 31 | import akka.stream.scaladsl.Source 32 | import akka.util.ByteString 33 | import javax.net.ssl.{KeyManagerFactory, SSLContext, SSLSocketFactory, TrustManagerFactory, X509TrustManager} 34 | import kamon.Kamon 35 | import kamon.instrumentation.akka.http.TracingDirectives 36 | import org.json4s.{DefaultFormats, native} 37 | import kamon.tag.Lookups.plain 38 | import kamon.trace.Trace 39 | import scala.concurrent.{ExecutionContext, Future} 40 | 41 | trait TestWebServer extends TracingDirectives { 42 | implicit val serialization = native.Serialization 43 | implicit val formats = DefaultFormats 44 | import Json4sSupport._ 45 | 46 | def startServer(interface: String, port: Int, https: Boolean = false)(implicit system: ActorSystem): WebServer = { 47 | import Endpoints._ 48 | 49 | implicit val ec: ExecutionContext = system.dispatcher 50 | implicit val materializer = ActorMaterializer() 51 | 52 | val routes = logRequest("routing-request") { 53 | get { 54 | path("v3" / "user" / IntNumber / "post" / IntNumber) { (_, _) => 55 | complete("OK") 56 | } ~ 57 | pathPrefix("extraction") { 58 | authenticateBasic("realm", credentials => Option("Okay")) { srt => 59 | (post | get) { 60 | pathPrefix("nested") { 61 | pathPrefix(IntNumber / "fixed") { num => 62 | pathPrefix("anchor" / IntNumber.? / JavaUUID / "fixed") { (number, uuid) => 63 | pathPrefix(LongNumber / HexIntNumber) { (longNum, hex) => 64 | complete("OK") 65 | } 66 | } 67 | } 68 | } ~ 69 | pathPrefix("concat") { 70 | path("fixed" ~ JavaUUID ~ HexIntNumber) { (uuid, num) => 71 | complete("OK") 72 | } 73 | } ~ 74 | pathPrefix("on-complete" / IntNumber) { _ => 75 | onComplete(Future("hello")) { _ => 76 | extract(samplingDecision) { decision => 77 | path("more-path") { 78 | complete(decision.toString) 79 | } 80 | } 81 | } 82 | } ~ 83 | pathPrefix("on-success" / IntNumber) { _ => 84 | onSuccess(Future("hello")) { text => 85 | pathPrefix("after") { 86 | complete(text) 87 | } 88 | } 89 | } ~ 90 | pathPrefix("complete-or-recover-with" / IntNumber) { _ => 91 | completeOrRecoverWith(Future("bad".charAt(10).toString)) { failure => 92 | pathPrefix("after") { 93 | failWith(failure) 94 | } 95 | } 96 | } ~ 97 | pathPrefix("complete-or-recover-with-success" / IntNumber) { _ => 98 | completeOrRecoverWith(Future("good")) { failure => 99 | pathPrefix("after") { 100 | failWith(failure) 101 | } 102 | } 103 | } ~ 104 | path("segment" / Segment){ segment => 105 | complete(HttpResponse(entity = HttpEntity(ContentTypes.`text/plain(UTF-8)`, segment))) 106 | } 107 | } 108 | } 109 | } ~ 110 | path(rootOk) { 111 | complete(OK) 112 | } ~ 113 | path(dummyPathOk) { 114 | complete(OK) 115 | } ~ 116 | path(dummyPathError) { 117 | complete(InternalServerError) 118 | } ~ 119 | path(traceOk) { 120 | operationName("user-supplied-operation") { 121 | complete(OK) 122 | } 123 | } ~ 124 | path(traceBadRequest) { 125 | complete(BadRequest) 126 | } ~ 127 | path(metricsOk) { 128 | complete(OK) 129 | } ~ 130 | path(metricsBadRequest) { 131 | complete(BadRequest) 132 | } ~ 133 | path(replyWithHeaders) { 134 | extractRequest { req => 135 | complete(req.headers.map(h => (h.name(), h.value())).toMap[String, String]) 136 | } 137 | } ~ 138 | path(basicContext) { 139 | complete { 140 | Map( 141 | "custom-string-key" -> Kamon.currentContext().getTag(plain("custom-string-key")), 142 | "trace-id" -> Kamon.currentSpan().trace.id.string 143 | ) 144 | } 145 | } ~ 146 | path(waitTen) { 147 | respondWithHeader(Connection("close")) { 148 | complete { 149 | Thread.sleep(5000) 150 | OK 151 | } 152 | } 153 | } ~ 154 | path(stream) { 155 | complete { 156 | val longStringContentStream = Source.fromIterator(() => 157 | Range(1, 16) 158 | .map(i => ByteString(100 * ('a' + i).toChar)) 159 | .iterator 160 | ) 161 | 162 | HttpResponse(entity = HttpEntity(ContentTypes.`text/plain(UTF-8)`, 1600, longStringContentStream)) 163 | } 164 | } ~ 165 | path("extra-header") { 166 | respondWithHeader(RawHeader("extra", "extra-header")) { 167 | complete(OK) 168 | } 169 | } ~ 170 | path("name-will-be-changed") { 171 | complete("OK") 172 | } 173 | } 174 | } 175 | 176 | if(https) 177 | new WebServer(interface, port, "https", Http().bindAndHandleAsync(Route.asyncHandler(routes), interface, port, httpContext())) 178 | else 179 | new WebServer(interface, port, "http", Http().bindAndHandle(routes, interface, port)) 180 | } 181 | 182 | def httpContext() = { 183 | val password = "kamon".toCharArray 184 | val ks = KeyStore.getInstance("PKCS12") 185 | ks.load(getClass.getClassLoader.getResourceAsStream("https/server.p12"), password) 186 | 187 | val keyManagerFactory = KeyManagerFactory.getInstance("SunX509") 188 | keyManagerFactory.init(ks, password) 189 | 190 | val context = SSLContext.getInstance("TLS") 191 | context.init(keyManagerFactory.getKeyManagers, null, new SecureRandom) 192 | 193 | new HttpsConnectionContext(context) 194 | } 195 | 196 | def clientSSL(): (SSLSocketFactory, X509TrustManager) = { 197 | val certStore = KeyStore.getInstance(KeyStore.getDefaultType) 198 | certStore.load(null, null) 199 | // only do this if you want to accept a custom root CA. Understand what you are doing! 200 | certStore.setCertificateEntry("ca", loadX509Certificate("https/rootCA.crt")) 201 | 202 | val certManagerFactory = TrustManagerFactory.getInstance("SunX509") 203 | certManagerFactory.init(certStore) 204 | 205 | val context = SSLContext.getInstance("TLS") 206 | context.init(null, certManagerFactory.getTrustManagers, new SecureRandom) 207 | 208 | (context.getSocketFactory, certManagerFactory.getTrustManagers.apply(0).asInstanceOf[X509TrustManager]) 209 | } 210 | 211 | def loadX509Certificate(resourceName: String): Certificate = 212 | CertificateFactory.getInstance("X.509").generateCertificate(getClass.getClassLoader.getResourceAsStream(resourceName)) 213 | 214 | def samplingDecision(ctx: RequestContext): Trace.SamplingDecision = 215 | Kamon.currentSpan().trace.samplingDecision 216 | 217 | object Endpoints { 218 | val rootOk: String = "" 219 | val dummyPathOk: String = "dummy-path" 220 | val dummyPathError: String = "dummy-path-error" 221 | val traceOk: String = "record-trace-metrics-ok" 222 | val traceBadRequest: String = "record-trace-metrics-bad-request" 223 | val metricsOk: String = "record-http-metrics-ok" 224 | val metricsBadRequest: String = "record-http-metrics-bad-request" 225 | val replyWithHeaders: String = "reply-with-headers" 226 | val basicContext: String = "basic-context" 227 | val waitTen: String = "wait" 228 | val stream: String = "stream" 229 | 230 | implicit class Converter(endpoint: String) { 231 | implicit def withSlash: String = "/" + endpoint 232 | } 233 | } 234 | 235 | class WebServer(val interface: String, val port: Int, val protocol: String, bindingFuture: Future[Http.ServerBinding])(implicit ec: ExecutionContext) { 236 | def shutdown(): Future[_] = { 237 | bindingFuture.flatMap(binding => binding.unbind()) 238 | } 239 | } 240 | 241 | } 242 | 243 | object TestWebServer extends TestWebServer 244 | -------------------------------------------------------------------------------- /kamon-akka-http/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | # ======================================= # 2 | # Kamon-Akka-Http Reference Configuration # 3 | # ======================================= # 4 | 5 | kamon.instrumentation.akka.http { 6 | 7 | # Settings to control the HTTP Server instrumentation. 8 | # 9 | # IMPORTANT: Besides the "initial-operation-name" and "unhandled-operation-name" settings, the entire configuration of 10 | # the HTTP Server Instrumentation is based on the constructs provided by the Kamon Instrumentation Common library 11 | # which will always fallback to the settings found under the "kamon.instrumentation.http-server.default" path. The 12 | # default settings have been included here to make them easy to find and understand in the context of this project and 13 | # commented out so that any changes to the default settings will actually have effect. 14 | # 15 | server { 16 | 17 | # 18 | # Configuration for HTTP context propagation. 19 | # 20 | propagation { 21 | 22 | # Enables or disables HTTP context propagation on this HTTP server instrumentation. Please note that if 23 | # propagation is disabled then some distributed tracing features will not be work as expected (e.g. Spans can 24 | # be created and reported but will not be linked across boundaries nor take trace identifiers from tags). 25 | #enabled = yes 26 | 27 | # HTTP propagation channel to b used by this instrumentation. Take a look at the kamon.propagation.http.default 28 | # configuration for more details on how to configure the detault HTTP context propagation. 29 | #channel = "default" 30 | } 31 | 32 | 33 | # 34 | # Configuration for HTTP server metrics collection. 35 | # 36 | metrics { 37 | 38 | # Enables collection of HTTP server metrics. When enabled the following metrics will be collected, assuming 39 | # that the instrumentation is fully compliant: 40 | # 41 | # - http.server.requets 42 | # - http.server.request.active 43 | # - http.server.request.size 44 | # - http.server.response.size 45 | # - http.server.connection.lifetime 46 | # - http.server.connection.usage 47 | # - http.server.connection.open 48 | # 49 | # All metrics have at least three tags: component, interface and port. Additionally, the http.server.requests 50 | # metric will also have a status_code tag with the status code group (1xx, 2xx and so on). 51 | # 52 | #enabled = yes 53 | } 54 | 55 | 56 | # 57 | # Configuration for HTTP request tracing. 58 | # 59 | tracing { 60 | 61 | # Enables HTTP request tracing. When enabled the instrumentation will create Spans for incoming requests 62 | # and finish them when the response is sent back to the clients. 63 | #enabled = yes 64 | 65 | # Select a context tag that provides a preferred trace identifier. The preferred trace identifier will be used 66 | # only if all these conditions are met: 67 | # - the context tag is present. 68 | # - there is no parent Span on the incoming context (i.e. this is the first service on the trace). 69 | # - the identifier is valid in accordance to the identity provider. 70 | #preferred-trace-id-tag = "none" 71 | 72 | # Enables collection of span metrics using the `span.processing-time` metric. 73 | #span-metrics = on 74 | 75 | # Select which tags should be included as span and span metric tags. The possible options are: 76 | # - span: the tag is added as a Span tag (i.e. using span.tag(...)) 77 | # - metric: the tag is added a a Span metric tag (i.e. using span.tagMetric(...)) 78 | # - off: the tag is not used. 79 | # 80 | tags { 81 | 82 | # Use the http.url tag. 83 | #url = span 84 | 85 | # Use the http.method tag. 86 | #method = metric 87 | 88 | # Use the http.status_code tag. 89 | #status-code = metric 90 | 91 | # Copy tags from the context into the Spans with the specified purpouse. For example, to copy a customer_type 92 | # tag from the context into the HTTP Server Span created by the instrumentation, the following configuration 93 | # should be added: 94 | # 95 | # from-context { 96 | # customer_type = span 97 | # } 98 | # 99 | from-context { 100 | 101 | } 102 | } 103 | 104 | # Controls writing trace and span identifiers to HTTP response headers sent by the instrumented servers. The 105 | # configuration can be set to either "none" to disable writing the identifiers on the response headers or to 106 | # the header name to be used when writing the identifiers. 107 | response-headers { 108 | 109 | # HTTP response header name for the trace identifier, or "none" to disable it. 110 | #trace-id = "trace-id" 111 | 112 | # HTTP response header name for the server span identifier, or "none" to disable it. 113 | #span-id = none 114 | } 115 | 116 | # Custom mappings between routes and operation names. 117 | operations { 118 | 119 | # The default operation name to be used when creating Spans to handle the HTTP server requests. In most 120 | # cases it is not possible to define an operation name right at the moment of starting the HTTP server Span 121 | # and in those cases, this operation name will be initially assigned to the Span. Instrumentation authors 122 | # should do their best effort to provide a suitable operation name or make use of the "mappings" facilities. 123 | #default = "http.server.request" 124 | 125 | # Provides custom mappings from HTTP paths into operation names. Meant to be used in cases where the bytecode 126 | # instrumentation is not able to provide a sensible operation name that is free of high cardinality values. 127 | # For example, with the following configuration: 128 | # mappings { 129 | # "/organization/*/user/*/profile" = "/organization/:orgID/user/:userID/profile" 130 | # "/events/*/rsvps" = "EventRSVPs" 131 | # } 132 | # 133 | # Requests to "/organization/3651/user/39652/profile" and "/organization/22234/user/54543/profile" will have 134 | # the same operation name "/organization/:orgID/user/:userID/profile". 135 | # 136 | # Similarly, requests to "/events/aaa-bb-ccc/rsvps" and "/events/1234/rsvps" will have the same operation 137 | # name "EventRSVPs". 138 | # 139 | # The patterns are expressed as globs and the operation names are free form. 140 | # 141 | mappings { 142 | 143 | } 144 | } 145 | } 146 | } 147 | 148 | # Settings to control the HTTP Client instrumentation 149 | # 150 | # IMPORTANT: The entire configuration of the HTTP Client Instrumentation is based on the constructs provided by the 151 | # Kamon Instrumentation Common library which will always fallback to the settings found under the 152 | # "kamon.instrumentation.http-client.default" path. The default settings have been included here to make them easy to 153 | # find and understand in the context of this project and commented out so that any changes to the default settings 154 | # will actually have effect. 155 | # 156 | client { 157 | 158 | # 159 | # Configuration for HTTP context propagation. 160 | # 161 | propagation { 162 | 163 | # Enables or disables HTTP context propagation on this HTTP server instrumentation. Please note that if 164 | # propagation is disabled then some distributed tracing features will not be work as expected (e.g. Spans can 165 | # be created and reported but will not be linked across boundaries nor take trace identifiers from tags). 166 | #enabled = yes 167 | 168 | # HTTP propagation channel to b used by this instrumentation. Take a look at the kamon.propagation.http.default 169 | # configuration for more details on how to configure the detault HTTP context propagation. 170 | #channel = "default" 171 | } 172 | 173 | tracing { 174 | 175 | # Enables HTTP request tracing. When enabled the instrumentation will create Spans for outgoing requests 176 | # and finish them when the response is received from the server. 177 | #enabled = yes 178 | 179 | # Enables collection of span metrics using the `span.processing-time` metric. 180 | #span-metrics = on 181 | 182 | # Select which tags should be included as span and span metric tags. The possible options are: 183 | # - span: the tag is added as a Span tag (i.e. using span.tag(...)) 184 | # - metric: the tag is added a a Span metric tag (i.e. using span.tagMetric(...)) 185 | # - off: the tag is not used. 186 | # 187 | tags { 188 | 189 | # Use the http.url tag. 190 | #url = span 191 | 192 | # Use the http.method tag. 193 | #method = metric 194 | 195 | # Use the http.status_code tag. 196 | #status-code = metric 197 | 198 | # Copy tags from the context into the Spans with the specified purpouse. For example, to copy a customer_type 199 | # tag from the context into the HTTP Server Span created by the instrumentation, the following configuration 200 | # should be added: 201 | # 202 | # from-context { 203 | # customer_type = span 204 | # } 205 | # 206 | from-context { 207 | 208 | } 209 | } 210 | 211 | operations { 212 | 213 | # The default operation name to be used when creating Spans to handle the HTTP client requests. The HTTP 214 | # Client instrumentation will always try to use the HTTP Operation Name Generator configured bellow to get 215 | # a name, but if it fails to generate it then this name will be used. 216 | #default = "http.client.request" 217 | 218 | # FQCN for a HttpOperationNameGenerator implementation, or ony of the following shorthand forms: 219 | # - hostname: Uses the request Host as the operation name. 220 | # - method: Uses the request HTTP method as the operation name. 221 | # 222 | #name-generator = "method" 223 | } 224 | } 225 | } 226 | 227 | 228 | } 229 | 230 | 231 | kanela.modules { 232 | akka-http { 233 | name = "Akka HTTP Instrumentation" 234 | description = "Provides context propagation, distributed tracing and HTTP client and server metrics for Akka HTTP" 235 | 236 | instrumentations = [ 237 | "kamon.instrumentation.akka.http.AkkaHttpServerInstrumentation" 238 | "kamon.instrumentation.akka.http.AkkaHttpClientInstrumentation" 239 | ] 240 | 241 | within = [ 242 | "akka.http.*" 243 | ] 244 | } 245 | } -------------------------------------------------------------------------------- /kamon-akka-http/src/test/scala/kamon/akka/http/AkkaHttpServerTracingSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * ========================================================================================= 3 | * Copyright © 2013-2016 the kamon project 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 6 | * except in compliance with the License. You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the 11 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | * either express or implied. See the License for the specific language governing permissions 13 | * and limitations under the License. 14 | * ========================================================================================= 15 | */ 16 | 17 | package kamon.akka.http 18 | 19 | import java.util.UUID 20 | 21 | import akka.actor.ActorSystem 22 | import akka.http.scaladsl.UseHttp2 23 | import akka.http.scaladsl.unmarshalling.Unmarshal 24 | import akka.stream.ActorMaterializer 25 | import javax.net.ssl.{HostnameVerifier, SSLSession} 26 | import kamon.testkit._ 27 | import kamon.tag.Lookups.{plain, plainBoolean, plainLong} 28 | import kamon.trace.Span.Mark 29 | import org.scalatest._ 30 | import org.scalatest.concurrent.{Eventually, ScalaFutures} 31 | 32 | import scala.concurrent.duration._ 33 | import okhttp3.{OkHttpClient, Request} 34 | 35 | class AkkaHttpServerTracingSpec extends WordSpecLike with Matchers with ScalaFutures with Inside with BeforeAndAfterAll 36 | with MetricInspection.Syntax with Reconfigure with TestWebServer with Eventually with OptionValues with TestSpanReporter { 37 | 38 | import TestWebServer.Endpoints._ 39 | 40 | implicit private val system = ActorSystem("http-server-instrumentation-spec") 41 | implicit private val executor = system.dispatcher 42 | implicit private val materializer = ActorMaterializer() 43 | 44 | val (sslSocketFactory, trustManager) = clientSSL() 45 | val okHttp = new OkHttpClient.Builder() 46 | .sslSocketFactory(sslSocketFactory, trustManager) 47 | .hostnameVerifier(new HostnameVerifier { override def verify(s: String, sslSession: SSLSession): Boolean = true }) 48 | .build() 49 | 50 | val timeoutTest: FiniteDuration = 5 second 51 | val interface = "127.0.0.1" 52 | val http1WebServer = startServer(interface, 8081, https = false) 53 | val http2WebServer = startServer(interface, 8082, https = true) 54 | 55 | testSuite("HTTP/1", http1WebServer) 56 | testSuite("HTTP/2", http2WebServer) 57 | 58 | def testSuite(httpVersion: String, server: WebServer) = { 59 | val interface = server.interface 60 | val port = server.port 61 | val protocol = server.protocol 62 | 63 | s"the Akka HTTP server instrumentation with ${httpVersion}" should { 64 | "create a server Span when receiving requests" in { 65 | val target = s"$protocol://$interface:$port/$dummyPathOk" 66 | okHttp.newCall(new Request.Builder().url(target).build()).execute() 67 | 68 | eventually(timeout(10 seconds)) { 69 | val span = testSpanReporter().nextSpan().value 70 | span.tags.get(plain("http.url")) shouldBe target 71 | span.metricTags.get(plain("component")) shouldBe "akka.http.server" 72 | span.metricTags.get(plain("http.method")) shouldBe "GET" 73 | span.metricTags.get(plainLong("http.status_code")) shouldBe 200L 74 | } 75 | } 76 | 77 | "not include variables in operation name" when { 78 | "including nested directives" in { 79 | val path = s"extraction/nested/42/fixed/anchor/32/${UUID.randomUUID().toString}/fixed/44/CafE" 80 | val expected = "/extraction/nested/{}/fixed/anchor/{}/{}/fixed/{}/{}" 81 | val target = s"$protocol://$interface:$port/$path" 82 | okHttp.newCall(new Request.Builder().url(target).build()).execute() 83 | 84 | eventually(timeout(10 seconds)) { 85 | val span = testSpanReporter().nextSpan().value 86 | span.operationName shouldBe expected 87 | } 88 | } 89 | 90 | "not fail when request url contains special regexp chars" in { 91 | val path = "extraction/segment/special**" 92 | val expected = "/extraction/segment/{}" 93 | val target = s"$protocol://$interface:$port/$path" 94 | val response = okHttp.newCall(new Request.Builder().url(target).build()).execute() 95 | 96 | response.code() shouldBe 200 97 | response.body().string() shouldBe "special**" 98 | 99 | eventually(timeout(10 seconds)) { 100 | val span = testSpanReporter().nextSpan().value 101 | span.operationName shouldBe expected 102 | } 103 | } 104 | 105 | "take a sampling decision when the routing tree hits an onComplete directive" in { 106 | val path = "extraction/on-complete/42/more-path" 107 | val expected = "/extraction/on-complete/{}/more-path" 108 | val target = s"$protocol://$interface:$port/$path" 109 | okHttp.newCall(new Request.Builder().url(target).build()).execute() 110 | 111 | eventually(timeout(10 seconds)) { 112 | val span = testSpanReporter().nextSpan().value 113 | span.operationName shouldBe expected 114 | } 115 | } 116 | 117 | "take a sampling decision when the routing tree hits an onSuccess directive" in { 118 | val path = "extraction/on-success/42/after" 119 | val expected = "/extraction/on-success/{}/after" 120 | val target = s"$protocol://$interface:$port/$path" 121 | okHttp.newCall(new Request.Builder().url(target).build()).execute() 122 | 123 | eventually(timeout(10 seconds)) { 124 | val span = testSpanReporter().nextSpan().value 125 | span.operationName shouldBe expected 126 | } 127 | } 128 | 129 | "take a sampling decision when the routing tree hits a completeOrRecoverWith directive with a failed future" in { 130 | val path = "extraction/complete-or-recover-with/42/after" 131 | val expected = "/extraction/complete-or-recover-with/{}/after" 132 | val target = s"$protocol://$interface:$port/$path" 133 | okHttp.newCall(new Request.Builder().url(target).build()).execute() 134 | 135 | eventually(timeout(10 seconds)) { 136 | val span = testSpanReporter().nextSpan().value 137 | span.operationName shouldBe expected 138 | } 139 | } 140 | 141 | "take a sampling decision when the routing tree hits a completeOrRecoverWith directive with a successful future" in { 142 | val path = "extraction/complete-or-recover-with-success/42/after" 143 | val expected = "/extraction/complete-or-recover-with-success/{}" 144 | val target = s"$protocol://$interface:$port/$path" 145 | okHttp.newCall(new Request.Builder().url(target).build()).execute() 146 | 147 | eventually(timeout(10 seconds)) { 148 | val span = testSpanReporter().nextSpan().value 149 | span.operationName shouldBe expected 150 | } 151 | } 152 | 153 | "including ambiguous nested directives" in { 154 | val path = s"v3/user/3/post/3" 155 | val expected = "/v3/user/{}/post/{}" 156 | val target = s"$protocol://$interface:$port/$path" 157 | okHttp.newCall(new Request.Builder().url(target).build()).execute() 158 | 159 | eventually(timeout(10 seconds)) { 160 | val span = testSpanReporter().nextSpan().value 161 | span.operationName shouldBe expected 162 | } 163 | } 164 | } 165 | 166 | "change the Span operation name when using the operationName directive" in { 167 | val target = s"$protocol://$interface:$port/$traceOk" 168 | okHttp.newCall(new Request.Builder().url(target).build()).execute() 169 | 170 | eventually(timeout(10 seconds)) { 171 | val span = testSpanReporter().nextSpan().value 172 | span.operationName shouldBe "user-supplied-operation" 173 | span.tags.get(plain("http.url")) shouldBe target 174 | span.metricTags.get(plain("component")) shouldBe "akka.http.server" 175 | span.metricTags.get(plain("http.method")) shouldBe "GET" 176 | span.metricTags.get(plainLong("http.status_code")) shouldBe 200L 177 | } 178 | } 179 | 180 | "mark spans as failed when request fails" in { 181 | val target = s"$protocol://$interface:$port/$dummyPathError" 182 | okHttp.newCall(new Request.Builder().url(target).build()).execute() 183 | 184 | eventually(timeout(10 seconds)) { 185 | val span = testSpanReporter().nextSpan().value 186 | span.operationName shouldBe s"/$dummyPathError" 187 | span.tags.get(plain("http.url")) shouldBe target 188 | span.metricTags.get(plain("component")) shouldBe "akka.http.server" 189 | span.metricTags.get(plain("http.method")) shouldBe "GET" 190 | span.metricTags.get(plainBoolean("error")) shouldBe true 191 | span.metricTags.get(plainLong("http.status_code")) shouldBe 500L 192 | } 193 | } 194 | 195 | "change the operation name to 'unhandled' when the response status code is 404" in { 196 | val target = s"$protocol://$interface:$port/unknown-path" 197 | okHttp.newCall(new Request.Builder().url(target).build()).execute() 198 | 199 | eventually(timeout(10 seconds)) { 200 | val span = testSpanReporter().nextSpan().value 201 | span.operationName shouldBe "unhandled" 202 | span.tags.get(plain("http.url")) shouldBe target 203 | span.metricTags.get(plain("component")) shouldBe "akka.http.server" 204 | span.metricTags.get(plain("http.method")) shouldBe "GET" 205 | span.metricTags.get(plainBoolean("error")) shouldBe false 206 | span.metricTags.get(plainLong("http.status_code")) shouldBe 404L 207 | } 208 | } 209 | 210 | "correctly time entity transfer timings" in { 211 | val target = s"$protocol://$interface:$port/$stream" 212 | okHttp.newCall(new Request.Builder().url(target).build()).execute() 213 | 214 | val span = eventually(timeout(10 seconds)) { 215 | val span = testSpanReporter().nextSpan().value 216 | span.operationName shouldBe "/stream" 217 | span 218 | } 219 | 220 | inside(span.marks){ 221 | case List(_ @ Mark(_, "http.response.ready")) => 222 | } 223 | 224 | span.tags.get(plain("http.url")) shouldBe target 225 | span.metricTags.get(plain("component")) shouldBe "akka.http.server" 226 | span.metricTags.get(plain("http.method")) shouldBe "GET" 227 | } 228 | 229 | "include the trace-id and keep all user-provided headers in the responses" in { 230 | val target = s"$protocol://$interface:$port/extra-header" 231 | val response = okHttp.newCall(new Request.Builder().url(target).build()).execute() 232 | 233 | response.headers().names() should contain allOf ( 234 | "trace-id", 235 | "extra" 236 | ) 237 | } 238 | 239 | "keep operation names provided by the HTTP Server instrumentation" in { 240 | val target = s"$protocol://$interface:$port/name-will-be-changed" 241 | okHttp.newCall(new Request.Builder().url(target).build()).execute() 242 | 243 | eventually(timeout(10 seconds)) { 244 | val span = testSpanReporter().nextSpan().value 245 | span.operationName shouldBe "named-via-config" 246 | } 247 | } 248 | } 249 | } 250 | 251 | override protected def afterAll(): Unit = { 252 | http1WebServer.shutdown() 253 | http2WebServer.shutdown() 254 | } 255 | } 256 | 257 | -------------------------------------------------------------------------------- /kamon-akka-http/src/main/scala-2.12/kamon/instrumentation/akka/http/AkkaHttpServerInstrumentation.scala: -------------------------------------------------------------------------------- 1 | package kamon.instrumentation.akka.http 2 | 3 | import java.util.concurrent.Callable 4 | 5 | import akka.http.scaladsl.marshalling.{ToResponseMarshallable, ToResponseMarshaller} 6 | import akka.http.scaladsl.model.StatusCodes.Redirection 7 | import akka.http.scaladsl.model.{HttpRequest, HttpResponse, Uri} 8 | import akka.http.scaladsl.server.PathMatcher.{Matched, Unmatched} 9 | import akka.http.scaladsl.server.directives.{BasicDirectives, CompleteOrRecoverWithMagnet, OnSuccessMagnet} 10 | import akka.http.scaladsl.server.directives.RouteDirectives.reject 11 | import akka.http.scaladsl.server._ 12 | import akka.http.scaladsl.server.util.Tupler 13 | import akka.http.scaladsl.util.FastFuture 14 | import kamon.Kamon 15 | import kamon.instrumentation.akka.http.HasMatchingContext.PathMatchingContext 16 | import kamon.instrumentation.context.{HasContext, InvokeWithCapturedContext} 17 | import kanela.agent.api.instrumentation.InstrumentationBuilder 18 | import kanela.agent.api.instrumentation.mixin.Initializer 19 | import kanela.agent.libs.net.bytebuddy.implementation.bind.annotation._ 20 | 21 | import scala.concurrent.{ExecutionContext, Future, Promise} 22 | import scala.util.control.NonFatal 23 | import scala.util.{Failure, Success, Try} 24 | import java.util.regex.Pattern 25 | 26 | import akka.NotUsed 27 | import akka.stream.scaladsl.Flow 28 | import kamon.context.Context 29 | import kanela.agent.libs.net.bytebuddy.matcher.ElementMatchers.isPublic 30 | 31 | 32 | class AkkaHttpServerInstrumentation extends InstrumentationBuilder { 33 | 34 | /** 35 | * When instrumenting bindAndHandle what we do is wrap the Flow[HttpRequest, HttpResponse, NotUsed] provided by 36 | * the user and add all the processing there. This is the part of the instrumentation that performs Context 37 | * propagation, tracing and gather metrics using the HttpServerInstrumentation packed in common. 38 | * 39 | * One important point about the HTTP Server instrumentation is that because it is almost impossible to have a proper 40 | * operation name before the request processing hits the routing tree, we are delaying the sampling decision to the 41 | * point at which we have some operation name. 42 | */ 43 | onType("akka.http.scaladsl.HttpExt") 44 | .advise(method("bindAndHandle"), classOf[HttpExtBindAndHandleAdvice]) 45 | 46 | /** 47 | * For the HTTP/2 instrumentation, since the parts where we can capture the interface/port and the actual flow 48 | * creation happen at different times we are wrapping the handler with the interface/port data and reading that 49 | * information when turning the handler function into a flow and wrapping it the same way we would for HTTP/1. 50 | */ 51 | onType("akka.http.scaladsl.Http2Ext") 52 | .advise(method("bindAndHandleAsync") and isPublic(), classOf[Http2ExtBindAndHandleAdvice]) 53 | 54 | onType("akka.http.impl.engine.http2.Http2Blueprint$") 55 | .intercept(method("handleWithStreamIdHeader"), Http2BlueprintInterceptor) 56 | 57 | /** 58 | * The rest of these sections are just about making sure that we can generate an appropriate operation name (i.e. free 59 | * of variables) and take a Sampling Decision in case none has been taken so far. 60 | */ 61 | onType("akka.http.scaladsl.server.RequestContextImpl") 62 | .mixin(classOf[HasMatchingContext.Mixin]) 63 | .intercept(method("copy"), RequestContextCopyInterceptor) 64 | 65 | onType("akka.http.scaladsl.server.directives.PathDirectives") 66 | .intercept(method("rawPathPrefix"), classOf[PathDirectivesRawPathPrefixInterceptor]) 67 | 68 | onType("akka.http.scaladsl.server.directives.FutureDirectives") 69 | .intercept(method("onComplete"), classOf[ResolveOperationNameOnRouteInterceptor]) 70 | 71 | onTypes("akka.http.scaladsl.server.directives.OnSuccessMagnet$", "akka.http.scaladsl.server.directives.CompleteOrRecoverWithMagnet$") 72 | .intercept(method("apply"), classOf[ResolveOperationNameOnRouteInterceptor]) 73 | 74 | onType("akka.http.scaladsl.server.directives.RouteDirectives") 75 | .intercept(method("complete"), classOf[ResolveOperationNameOnRouteInterceptor]) 76 | .intercept(method("redirect"), classOf[ResolveOperationNameOnRouteInterceptor]) 77 | .intercept(method("failWith"), classOf[ResolveOperationNameOnRouteInterceptor]) 78 | 79 | 80 | /** 81 | * This allows us to keep the right Context when Futures go through Akka HTTP's FastFuture and transformantions made 82 | * to them. Without this, it might happen that when a Future is already completed and used on any of the Futures 83 | * directives we might get a Context mess up. 84 | */ 85 | onTypes("akka.http.scaladsl.util.FastFuture$FulfilledFuture", "akka.http.scaladsl.util.FastFuture$ErrorFuture") 86 | .mixin(classOf[HasContext.MixinWithInitializer]) 87 | .advise(method("transform"), InvokeWithCapturedContext) 88 | .advise(method("transformWith"), InvokeWithCapturedContext) 89 | .advise(method("onComplete"), InvokeWithCapturedContext) 90 | 91 | onType("akka.http.scaladsl.util.FastFuture$") 92 | .intercept(method("transformWith$extension1"), FastFutureTransformWithAdvice) 93 | } 94 | 95 | trait HasMatchingContext { 96 | def defaultOperationName: String 97 | def matchingContext: Seq[PathMatchingContext] 98 | def setMatchingContext(ctx: Seq[PathMatchingContext]): Unit 99 | def setDefaultOperationName(defaultOperationName: String): Unit 100 | def prependMatchingContext(matched: PathMatchingContext): Unit 101 | } 102 | 103 | object HasMatchingContext { 104 | 105 | case class PathMatchingContext ( 106 | fullPath: String, 107 | matched: Matched[_] 108 | ) 109 | 110 | class Mixin(var matchingContext: Seq[PathMatchingContext], var defaultOperationName: String) extends HasMatchingContext { 111 | 112 | override def setMatchingContext(matchingContext: Seq[PathMatchingContext]): Unit = 113 | this.matchingContext = matchingContext 114 | 115 | override def setDefaultOperationName(defaultOperationName: String): Unit = 116 | this.defaultOperationName = defaultOperationName 117 | 118 | override def prependMatchingContext(matched: PathMatchingContext): Unit = 119 | matchingContext = matched +: matchingContext 120 | 121 | @Initializer 122 | def initialize(): Unit = 123 | matchingContext = Seq.empty 124 | } 125 | } 126 | 127 | class ResolveOperationNameOnRouteInterceptor 128 | object ResolveOperationNameOnRouteInterceptor { 129 | import akka.http.scaladsl.util.FastFuture._ 130 | 131 | // We are replacing some of the basic directives here to ensure that we will resolve both the Sampling Decision and 132 | // the operation name before the request gets to the actual handling code (presumably inside of a "complete" 133 | // directive. 134 | 135 | def complete(m: => ToResponseMarshallable): StandardRoute = 136 | StandardRoute(resolveOperationName(_).complete(m)) 137 | 138 | def redirect(uri: Uri, redirectionType: Redirection): StandardRoute = 139 | StandardRoute(resolveOperationName(_).redirect(uri, redirectionType)) 140 | 141 | def failWith(error: Throwable): StandardRoute = { 142 | Kamon.currentSpan().fail(error) 143 | StandardRoute(resolveOperationName(_).fail(error)) 144 | } 145 | 146 | def onComplete[T](future: => Future[T]): Directive1[Try[T]] = 147 | Directive { inner => ctx => 148 | import ctx.executionContext 149 | resolveOperationName(ctx) 150 | future.fast.transformWith(t => inner(Tuple1(t))(ctx)) 151 | } 152 | 153 | def apply[T](future: => Future[T])(implicit tupler: Tupler[T]): OnSuccessMagnet { type Out = tupler.Out } = 154 | new OnSuccessMagnet { 155 | type Out = tupler.Out 156 | val directive = Directive[tupler.Out] { inner => ctx => 157 | import ctx.executionContext 158 | resolveOperationName(ctx) 159 | future.fast.flatMap(t => inner(tupler(t))(ctx)) 160 | }(tupler.OutIsTuple) 161 | } 162 | 163 | def apply[T](future: => Future[T])(implicit m: ToResponseMarshaller[T]): CompleteOrRecoverWithMagnet = 164 | new CompleteOrRecoverWithMagnet { 165 | val directive = Directive[Tuple1[Throwable]] { inner => ctx => 166 | import ctx.executionContext 167 | resolveOperationName(ctx) 168 | future.fast.transformWith { 169 | case Success(res) => ctx.complete(res) 170 | case Failure(error) => inner(Tuple1(error))(ctx) 171 | } 172 | } 173 | } 174 | 175 | private def resolveOperationName(requestContext: RequestContext): RequestContext = { 176 | 177 | // We will only change the operation name if the last edit made to it was an automatic one. At this point, the only 178 | // way in which the operation name might have changed is if the user changed it with the operationName directive or 179 | // by accessing the Span and changing it directly there, so we wouldn't want to overwrite that. 180 | 181 | Kamon.currentContext().get(LastAutomaticOperationNameEdit.Key).foreach(lastEdit => { 182 | val currentSpan = Kamon.currentSpan() 183 | 184 | if(lastEdit.allowAutomaticChanges) { 185 | if(currentSpan.operationName() == lastEdit.operationName) { 186 | val allMatches = requestContext.asInstanceOf[HasMatchingContext].matchingContext.reverse.map(singleMatch) 187 | val operationName = allMatches.mkString("") 188 | 189 | if(operationName.nonEmpty) { 190 | currentSpan 191 | .name(operationName) 192 | .takeSamplingDecision() 193 | 194 | lastEdit.operationName = operationName 195 | } 196 | } else { 197 | lastEdit.allowAutomaticChanges = false 198 | } 199 | } else { 200 | currentSpan.takeSamplingDecision() 201 | } 202 | }) 203 | 204 | requestContext 205 | } 206 | 207 | private def singleMatch(matching: PathMatchingContext): String = { 208 | val rest = matching.matched.pathRest.toString() 209 | val consumedCount = matching.fullPath.length - rest.length 210 | val consumedSegment = matching.fullPath.substring(0, consumedCount) 211 | 212 | matching.matched.extractions match { 213 | case () => //string segment matched 214 | consumedSegment 215 | case tuple: Product => 216 | val values = tuple.productIterator.toList map { 217 | case Some(x) => List(x.toString) 218 | case None => Nil 219 | case long: Long => List(long.toString, long.toHexString) 220 | case int: Int => List(int.toString, int.toHexString) 221 | case a: Any => List(a.toString) 222 | } 223 | values.flatten.fold(consumedSegment) { (full, value) => 224 | val r = "(?i)(^|/)" + Pattern.quote(value) + "($|/)" 225 | full.replaceFirst(r, "$1{}$2") 226 | } 227 | } 228 | } 229 | } 230 | 231 | /** 232 | * Tracks the last operation name that was automatically assigned to an operation via instrumentation. The 233 | * instrumentation might assign a name to the operations via settings on the HTTP Server instrumentation instance or 234 | * via the Path directives instrumentation, but might never reassign a name if the user somehow assigned their own name 235 | * to the operation. Users chan change operation names by: 236 | * - Using operation mappings via configuration of the HTTP Server. 237 | * - Providing a custom HTTP Operation Name Generator for the server. 238 | * - Using the "operationName" directive. 239 | * - Directly accessing the Span for the current operation and changing the name on it. 240 | * 241 | */ 242 | class LastAutomaticOperationNameEdit( 243 | @volatile var operationName: String, 244 | @volatile var allowAutomaticChanges: Boolean 245 | ) 246 | 247 | object LastAutomaticOperationNameEdit { 248 | val Key = Context.key[Option[LastAutomaticOperationNameEdit]]("laone", None) 249 | 250 | def apply(operationName: String, allowAutomaticChanges: Boolean): LastAutomaticOperationNameEdit = 251 | new LastAutomaticOperationNameEdit(operationName, allowAutomaticChanges) 252 | } 253 | 254 | object RequestContextCopyInterceptor { 255 | 256 | @RuntimeType 257 | def copy(@This context: RequestContext, @SuperCall copyCall: Callable[RequestContext]): RequestContext = { 258 | val copiedRequestContext = copyCall.call() 259 | copiedRequestContext.asInstanceOf[HasMatchingContext].setMatchingContext(context.asInstanceOf[HasMatchingContext].matchingContext) 260 | copiedRequestContext 261 | } 262 | } 263 | 264 | class PathDirectivesRawPathPrefixInterceptor 265 | object PathDirectivesRawPathPrefixInterceptor { 266 | import BasicDirectives._ 267 | 268 | def rawPathPrefix[T](@Argument(0) matcher: PathMatcher[T]): Directive[T] = { 269 | implicit val LIsTuple = matcher.ev 270 | 271 | extract(ctx => { 272 | val fullPath = ctx.unmatchedPath.toString() 273 | val matching = matcher(ctx.unmatchedPath) 274 | matching match { 275 | case m: Matched[_] => 276 | ctx.asInstanceOf[HasMatchingContext].prependMatchingContext(PathMatchingContext(fullPath, m)) 277 | case _ => 278 | } 279 | matching 280 | }).flatMap { 281 | case Matched(rest, values) => tprovide(values) & mapRequestContext(_ withUnmatchedPath rest) 282 | case Unmatched => reject 283 | } 284 | } 285 | } 286 | 287 | object FastFutureTransformWithAdvice { 288 | 289 | @RuntimeType 290 | def transformWith[A, B](@Argument(0) future: Future[A], @Argument(1) s: A => Future[B], @Argument(2) f: Throwable => Future[B], 291 | @Argument(3) ec: ExecutionContext, @SuperCall zuper: Callable[Future[B]]): Future[B] = { 292 | 293 | def strictTransform[T](x: T, f: T => Future[B]) = 294 | try f(x) 295 | catch { case NonFatal(e) => FastFuture.failed(e) } 296 | 297 | // If we get a FulfilledFuture or ErrorFuture, those will have the HasContext mixin, 298 | // otherwise we are getting a regular Future which has the context mixed into its value. 299 | if(future.isInstanceOf[HasContext]) 300 | zuper.call() 301 | else { 302 | future.value match { 303 | case None => 304 | val p = Promise[B]() 305 | future.onComplete { 306 | case Success(a) => p completeWith strictTransform(a, s) 307 | case Failure(e) => p completeWith strictTransform(e, f) 308 | }(ec) 309 | p.future 310 | case Some(value) => 311 | // This is possible because of the Future's instrumentation 312 | val futureContext = value.asInstanceOf[HasContext].context 313 | val scope = Kamon.storeContext(futureContext) 314 | 315 | val transformedFuture = value match { 316 | case Success(a) => strictTransform(a, s) 317 | case Failure(e) => strictTransform(e, f) 318 | } 319 | 320 | scope.close() 321 | transformedFuture 322 | } 323 | } 324 | } 325 | } 326 | 327 | 328 | object Http2BlueprintInterceptor { 329 | 330 | case class HandlerWithEndpoint(interface: String, port: Int, handler: HttpRequest => Future[HttpResponse]) 331 | extends (HttpRequest => Future[HttpResponse]) { 332 | 333 | override def apply(request: HttpRequest): Future[HttpResponse] = handler(request) 334 | } 335 | 336 | @RuntimeType 337 | def handleWithStreamIdHeader(@Argument(1) handler: HttpRequest => Future[HttpResponse], 338 | @SuperCall zuper: Callable[Flow[HttpRequest, HttpResponse, NotUsed]]): Flow[HttpRequest, HttpResponse, NotUsed] = { 339 | 340 | handler match { 341 | case HandlerWithEndpoint(interface, port, _) => 342 | ServerFlowWrapper(zuper.call(), interface, port) 343 | 344 | case _ => 345 | zuper.call() 346 | } 347 | } 348 | } -------------------------------------------------------------------------------- /kamon-akka-http/src/main/scala-2.11/kamon/instrumentation/akka/http/AkkaHttpServerInstrumentation.scala: -------------------------------------------------------------------------------- 1 | package kamon.instrumentation.akka.http 2 | 3 | import java.util.concurrent.Callable 4 | 5 | import akka.http.scaladsl.marshalling.{ToResponseMarshallable, ToResponseMarshaller} 6 | import akka.http.scaladsl.model.StatusCodes.Redirection 7 | import akka.http.scaladsl.model.{HttpRequest, HttpResponse, Uri} 8 | import akka.http.scaladsl.server.PathMatcher.{Matched, Unmatched} 9 | import akka.http.scaladsl.server.directives.{BasicDirectives, CompleteOrRecoverWithMagnet, OnSuccessMagnet} 10 | import akka.http.scaladsl.server.directives.RouteDirectives.reject 11 | import akka.http.scaladsl.server._ 12 | import akka.http.scaladsl.server.util.Tupler 13 | import akka.http.scaladsl.util.FastFuture 14 | import kamon.Kamon 15 | import kamon.instrumentation.akka.http.HasMatchingContext.PathMatchingContext 16 | import kamon.instrumentation.context.{HasContext, InvokeWithCapturedContext} 17 | import kanela.agent.api.instrumentation.InstrumentationBuilder 18 | import kanela.agent.api.instrumentation.mixin.Initializer 19 | import kanela.agent.libs.net.bytebuddy.implementation.bind.annotation._ 20 | 21 | import scala.concurrent.{ExecutionContext, Future, Promise} 22 | import scala.util.control.NonFatal 23 | import scala.util.{Failure, Success, Try} 24 | import java.util.regex.Pattern 25 | 26 | import akka.NotUsed 27 | import akka.stream.scaladsl.Flow 28 | import kamon.context.Context 29 | import kanela.agent.libs.net.bytebuddy.matcher.ElementMatchers.isPublic 30 | 31 | 32 | class AkkaHttpServerInstrumentation extends InstrumentationBuilder { 33 | 34 | /** 35 | * When instrumenting bindAndHandle what we do is wrap the Flow[HttpRequest, HttpResponse, NotUsed] provided by 36 | * the user and add all the processing there. This is the part of the instrumentation that performs Context 37 | * propagation, tracing and gather metrics using the HttpServerInstrumentation packed in common. 38 | * 39 | * One important point about the HTTP Server instrumentation is that because it is almost impossible to have a proper 40 | * operation name before the request processing hits the routing tree, we are delaying the sampling decision to the 41 | * point at which we have some operation name. 42 | */ 43 | onType("akka.http.scaladsl.HttpExt") 44 | .advise(method("bindAndHandle"), classOf[HttpExtBindAndHandleAdvice]) 45 | 46 | /** 47 | * For the HTTP/2 instrumentation, since the parts where we can capture the interface/port and the actual flow 48 | * creation happen at different times we are wrapping the handler with the interface/port data and reading that 49 | * information when turning the handler function into a flow and wrapping it the same way we would for HTTP/1. 50 | */ 51 | onType("akka.http.scaladsl.Http2Ext") 52 | .advise(method("bindAndHandleAsync") and isPublic(), classOf[Http2ExtBindAndHandleAdvice]) 53 | 54 | onType("akka.http.impl.engine.http2.Http2Blueprint$") 55 | .intercept(method("handleWithStreamIdHeader"), Http2BlueprintInterceptor) 56 | 57 | /** 58 | * The rest of these sections are just about making sure that we can generate an appropriate operation name (i.e. free 59 | * of variables) and take a Sampling Decision in case none has been taken so far. 60 | */ 61 | onType("akka.http.scaladsl.server.RequestContextImpl") 62 | .mixin(classOf[HasMatchingContext.Mixin]) 63 | .intercept(method("copy"), RequestContextCopyInterceptor) 64 | 65 | onType("akka.http.scaladsl.server.directives.PathDirectives$class") 66 | .intercept(method("rawPathPrefix"), classOf[PathDirectivesRawPathPrefixInterceptor]) 67 | 68 | onType("akka.http.scaladsl.server.directives.FutureDirectives$class") 69 | .intercept(method("onComplete"), classOf[ResolveOperationNameOnRouteInterceptor]) 70 | 71 | onTypes("akka.http.scaladsl.server.directives.OnSuccessMagnet$", "akka.http.scaladsl.server.directives.CompleteOrRecoverWithMagnet$") 72 | .intercept(method("apply"), classOf[ResolveOperationNameOnRouteInterceptor]) 73 | 74 | onType("akka.http.scaladsl.server.directives.RouteDirectives$class") 75 | .intercept(method("complete"), classOf[ResolveOperationNameOnRouteInterceptor]) 76 | .intercept(method("redirect"), classOf[ResolveOperationNameOnRouteInterceptor]) 77 | .intercept(method("failWith"), classOf[ResolveOperationNameOnRouteInterceptor]) 78 | 79 | 80 | /** 81 | * This allows us to keep the right Context when Futures go through Akka HTTP's FastFuture and transformantions made 82 | * to them. Without this, it might happen that when a Future is already completed and used on any of the Futures 83 | * directives we might get a Context mess up. 84 | */ 85 | onTypes("akka.http.scaladsl.util.FastFuture$FulfilledFuture", "akka.http.scaladsl.util.FastFuture$ErrorFuture") 86 | .mixin(classOf[HasContext.MixinWithInitializer]) 87 | .advise(method("transform"), InvokeWithCapturedContext) 88 | .advise(method("transformWith"), InvokeWithCapturedContext) 89 | .advise(method("onComplete"), InvokeWithCapturedContext) 90 | 91 | onType("akka.http.scaladsl.util.FastFuture$") 92 | .intercept(method("transformWith$extension1"), FastFutureTransformWithAdvice) 93 | } 94 | 95 | trait HasMatchingContext { 96 | def defaultOperationName: String 97 | def matchingContext: Seq[PathMatchingContext] 98 | def setMatchingContext(ctx: Seq[PathMatchingContext]): Unit 99 | def setDefaultOperationName(defaultOperationName: String): Unit 100 | def prependMatchingContext(matched: PathMatchingContext): Unit 101 | } 102 | 103 | object HasMatchingContext { 104 | 105 | case class PathMatchingContext ( 106 | fullPath: String, 107 | matched: Matched[_] 108 | ) 109 | 110 | class Mixin(var matchingContext: Seq[PathMatchingContext], var defaultOperationName: String) extends HasMatchingContext { 111 | 112 | override def setMatchingContext(matchingContext: Seq[PathMatchingContext]): Unit = 113 | this.matchingContext = matchingContext 114 | 115 | override def setDefaultOperationName(defaultOperationName: String): Unit = 116 | this.defaultOperationName = defaultOperationName 117 | 118 | override def prependMatchingContext(matched: PathMatchingContext): Unit = 119 | matchingContext = matched +: matchingContext 120 | 121 | @Initializer 122 | def initialize(): Unit = 123 | matchingContext = Seq.empty 124 | } 125 | } 126 | 127 | class ResolveOperationNameOnRouteInterceptor 128 | object ResolveOperationNameOnRouteInterceptor { 129 | import akka.http.scaladsl.util.FastFuture._ 130 | 131 | // We are replacing some of the basic directives here to ensure that we will resolve both the Sampling Decision and 132 | // the operation name before the request gets to the actual handling code (presumably inside of a "complete" 133 | // directive. 134 | 135 | def complete(@Argument(1) m: => ToResponseMarshallable): StandardRoute = 136 | StandardRoute(resolveOperationName(_).complete(m)) 137 | 138 | def redirect(@Argument(1) uri: Uri, @Argument(2) redirectionType: Redirection): StandardRoute = 139 | StandardRoute(resolveOperationName(_).redirect(uri, redirectionType)) 140 | 141 | def failWith(@Argument(1) error: Throwable): StandardRoute = { 142 | Kamon.currentSpan().fail(error) 143 | StandardRoute(resolveOperationName(_).fail(error)) 144 | } 145 | 146 | def onComplete[T](@Argument(1) future: => Future[T]): Directive1[Try[T]] = 147 | Directive { inner => ctx => 148 | import ctx.executionContext 149 | resolveOperationName(ctx) 150 | future.fast.transformWith(t => inner(Tuple1(t))(ctx)) 151 | } 152 | 153 | def apply[T](future: => Future[T])(implicit tupler: Tupler[T]): OnSuccessMagnet { type Out = tupler.Out } = 154 | new OnSuccessMagnet { 155 | type Out = tupler.Out 156 | val directive = Directive[tupler.Out] { inner => ctx => 157 | import ctx.executionContext 158 | resolveOperationName(ctx) 159 | future.fast.flatMap(t => inner(tupler(t))(ctx)) 160 | }(tupler.OutIsTuple) 161 | } 162 | 163 | def apply[T](future: => Future[T])(implicit m: ToResponseMarshaller[T]): CompleteOrRecoverWithMagnet = 164 | new CompleteOrRecoverWithMagnet { 165 | val directive = Directive[Tuple1[Throwable]] { inner => ctx => 166 | import ctx.executionContext 167 | resolveOperationName(ctx) 168 | future.fast.transformWith { 169 | case Success(res) => ctx.complete(res) 170 | case Failure(error) => inner(Tuple1(error))(ctx) 171 | } 172 | } 173 | } 174 | 175 | private def resolveOperationName(requestContext: RequestContext): RequestContext = { 176 | 177 | // We will only change the operation name if the last edit made to it was an automatic one. At this point, the only 178 | // way in which the operation name might have changed is if the user changed it with the operationName directive or 179 | // by accessing the Span and changing it directly there, so we wouldn't want to overwrite that. 180 | 181 | Kamon.currentContext().get(LastAutomaticOperationNameEdit.Key).foreach(lastEdit => { 182 | val currentSpan = Kamon.currentSpan() 183 | 184 | if(lastEdit.allowAutomaticChanges) { 185 | if(currentSpan.operationName() == lastEdit.operationName) { 186 | val allMatches = requestContext.asInstanceOf[HasMatchingContext].matchingContext.reverse.map(singleMatch) 187 | val operationName = allMatches.mkString("") 188 | 189 | if(operationName.nonEmpty) { 190 | currentSpan 191 | .name(operationName) 192 | .takeSamplingDecision() 193 | 194 | lastEdit.operationName = operationName 195 | } 196 | } else { 197 | lastEdit.allowAutomaticChanges = false 198 | } 199 | } else { 200 | currentSpan.takeSamplingDecision() 201 | } 202 | }) 203 | 204 | requestContext 205 | } 206 | 207 | private def singleMatch(matching: PathMatchingContext): String = { 208 | val rest = matching.matched.pathRest.toString() 209 | val consumedCount = matching.fullPath.length - rest.length 210 | val consumedSegment = matching.fullPath.substring(0, consumedCount) 211 | 212 | matching.matched.extractions match { 213 | case () => //string segment matched 214 | consumedSegment 215 | case tuple: Product => 216 | val values = tuple.productIterator.toList map { 217 | case Some(x) => List(x.toString) 218 | case None => Nil 219 | case long: Long => List(long.toString, long.toHexString) 220 | case int: Int => List(int.toString, int.toHexString) 221 | case a: Any => List(a.toString) 222 | } 223 | values.flatten.fold(consumedSegment) { (full, value) => 224 | val r = "(?i)(^|/)" + Pattern.quote(value) + "($|/)" 225 | full.replaceFirst(r, "$1{}$2") 226 | } 227 | } 228 | } 229 | } 230 | 231 | /** 232 | * Tracks the last operation name that was automatically assigned to an operation via instrumentation. The 233 | * instrumentation might assign a name to the operations via settings on the HTTP Server instrumentation instance or 234 | * via the Path directives instrumentation, but might never reassign a name if the user somehow assigned their own name 235 | * to the operation. Users chan change operation names by: 236 | * - Using operation mappings via configuration of the HTTP Server. 237 | * - Providing a custom HTTP Operation Name Generator for the server. 238 | * - Using the "operationName" directive. 239 | * - Directly accessing the Span for the current operation and changing the name on it. 240 | * 241 | */ 242 | class LastAutomaticOperationNameEdit( 243 | @volatile var operationName: String, 244 | @volatile var allowAutomaticChanges: Boolean 245 | ) 246 | 247 | object LastAutomaticOperationNameEdit { 248 | val Key = Context.key[Option[LastAutomaticOperationNameEdit]]("laone", None) 249 | 250 | def apply(operationName: String, allowAutomaticChanges: Boolean): LastAutomaticOperationNameEdit = 251 | new LastAutomaticOperationNameEdit(operationName, allowAutomaticChanges) 252 | } 253 | 254 | object RequestContextCopyInterceptor { 255 | 256 | @RuntimeType 257 | def copy(@This context: RequestContext, @SuperCall copyCall: Callable[RequestContext]): RequestContext = { 258 | val copiedRequestContext = copyCall.call() 259 | copiedRequestContext.asInstanceOf[HasMatchingContext].setMatchingContext(context.asInstanceOf[HasMatchingContext].matchingContext) 260 | copiedRequestContext 261 | } 262 | } 263 | 264 | class PathDirectivesRawPathPrefixInterceptor 265 | object PathDirectivesRawPathPrefixInterceptor { 266 | import BasicDirectives._ 267 | 268 | @RuntimeType 269 | def rawPathPrefix[T](@Argument(1) matcher: PathMatcher[T]): Directive[T] = { 270 | implicit val LIsTuple = matcher.ev 271 | 272 | extract(ctx => { 273 | val fullPath = ctx.unmatchedPath.toString() 274 | val matching = matcher(ctx.unmatchedPath) 275 | matching match { 276 | case m: Matched[_] => 277 | ctx.asInstanceOf[HasMatchingContext].prependMatchingContext(PathMatchingContext(fullPath, m)) 278 | case _ => 279 | } 280 | matching 281 | }).flatMap { 282 | case Matched(rest, values) => tprovide(values) & mapRequestContext(_ withUnmatchedPath rest) 283 | case Unmatched => reject 284 | } 285 | } 286 | } 287 | 288 | object FastFutureTransformWithAdvice { 289 | 290 | @RuntimeType 291 | def transformWith[A, B](@Argument(0) future: Future[A], @Argument(1) s: A => Future[B], @Argument(2) f: Throwable => Future[B], 292 | @Argument(3) ec: ExecutionContext, @SuperCall zuper: Callable[Future[B]]): Future[B] = { 293 | 294 | def strictTransform[T](x: T, f: T => Future[B]) = 295 | try f(x) 296 | catch { case NonFatal(e) => FastFuture.failed(e) } 297 | 298 | // If we get a FulfilledFuture or ErrorFuture, those will have the HasContext mixin, 299 | // otherwise we are getting a regular Future which has the context mixed into its value. 300 | if(future.isInstanceOf[HasContext]) 301 | zuper.call() 302 | else { 303 | future.value match { 304 | case None => 305 | val p = Promise[B]() 306 | future.onComplete { 307 | case Success(a) => p completeWith strictTransform(a, s) 308 | case Failure(e) => p completeWith strictTransform(e, f) 309 | }(ec) 310 | p.future 311 | case Some(value) => 312 | // This is possible because of the Future's instrumentation 313 | val futureContext = value.asInstanceOf[HasContext].context 314 | val scope = Kamon.storeContext(futureContext) 315 | 316 | val transformedFuture = value match { 317 | case Success(a) => strictTransform(a, s) 318 | case Failure(e) => strictTransform(e, f) 319 | } 320 | 321 | scope.close() 322 | transformedFuture 323 | } 324 | } 325 | } 326 | } 327 | 328 | object Http2BlueprintInterceptor { 329 | 330 | case class HandlerWithEndpoint(interface: String, port: Int, handler: HttpRequest => Future[HttpResponse]) 331 | extends (HttpRequest => Future[HttpResponse]) { 332 | 333 | override def apply(request: HttpRequest): Future[HttpResponse] = handler(request) 334 | } 335 | 336 | @RuntimeType 337 | def handleWithStreamIdHeader(@Argument(1) handler: HttpRequest => Future[HttpResponse], 338 | @SuperCall zuper: Callable[Flow[HttpRequest, HttpResponse, NotUsed]]): Flow[HttpRequest, HttpResponse, NotUsed] = { 339 | 340 | handler match { 341 | case HandlerWithEndpoint(interface, port, _) => 342 | ServerFlowWrapper(zuper.call(), interface, port) 343 | 344 | case _ => 345 | zuper.call() 346 | } 347 | } 348 | } -------------------------------------------------------------------------------- /kamon-akka-http/src/main/scala-2.13/kamon/instrumentation/akka/http/AkkaHttpServerInstrumentation.scala: -------------------------------------------------------------------------------- 1 | package kamon.instrumentation.akka.http 2 | 3 | import java.util.concurrent.Callable 4 | 5 | import akka.http.scaladsl.marshalling.{ToResponseMarshallable, ToResponseMarshaller} 6 | import akka.http.scaladsl.model.StatusCodes.Redirection 7 | import akka.http.scaladsl.model.{HttpRequest, HttpResponse, Uri} 8 | import akka.http.scaladsl.server.PathMatcher.{Matched, Unmatched} 9 | import akka.http.scaladsl.server.directives.{BasicDirectives, CompleteOrRecoverWithMagnet, OnSuccessMagnet} 10 | import akka.http.scaladsl.server.directives.RouteDirectives.reject 11 | import akka.http.scaladsl.server._ 12 | import akka.http.scaladsl.server.util.Tupler 13 | import akka.http.scaladsl.util.FastFuture 14 | import kamon.Kamon 15 | import kamon.instrumentation.akka.http.HasMatchingContext.PathMatchingContext 16 | import kamon.instrumentation.context.{HasContext, InvokeWithCapturedContext} 17 | import kanela.agent.api.instrumentation.InstrumentationBuilder 18 | import kanela.agent.api.instrumentation.mixin.Initializer 19 | import kanela.agent.libs.net.bytebuddy.implementation.bind.annotation._ 20 | 21 | import scala.concurrent.{Batchable, ExecutionContext, Future, Promise} 22 | import scala.util.control.NonFatal 23 | import scala.util.{Failure, Success, Try} 24 | import java.util.regex.Pattern 25 | 26 | import akka.NotUsed 27 | import akka.stream.scaladsl.Flow 28 | import kamon.context.Context 29 | import kanela.agent.libs.net.bytebuddy.matcher.ElementMatchers.isPublic 30 | 31 | 32 | class AkkaHttpServerInstrumentation extends InstrumentationBuilder { 33 | 34 | /** 35 | * When instrumenting bindAndHandle what we do is wrap the Flow[HttpRequest, HttpResponse, NotUsed] provided by 36 | * the user and add all the processing there. This is the part of the instrumentation that performs Context 37 | * propagation, tracing and gather metrics using the HttpServerInstrumentation packed in common. 38 | * 39 | * One important point about the HTTP Server instrumentation is that because it is almost impossible to have a proper 40 | * operation name before the request processing hits the routing tree, we are delaying the sampling decision to the 41 | * point at which we have some operation name. 42 | */ 43 | onType("akka.http.scaladsl.HttpExt") 44 | .advise(method("bindAndHandle"), classOf[HttpExtBindAndHandleAdvice]) 45 | 46 | /** 47 | * For the HTTP/2 instrumentation, since the parts where we can capture the interface/port and the actual flow 48 | * creation happen at different times we are wrapping the handler with the interface/port data and reading that 49 | * information when turning the handler function into a flow and wrapping it the same way we would for HTTP/1. 50 | */ 51 | onType("akka.http.scaladsl.Http2Ext") 52 | .advise(method("bindAndHandleAsync") and isPublic(), classOf[Http2ExtBindAndHandleAdvice]) 53 | 54 | onType("akka.http.impl.engine.http2.Http2Blueprint$") 55 | .intercept(method("handleWithStreamIdHeader"), Http2BlueprintInterceptor) 56 | 57 | /** 58 | * The rest of these sections are just about making sure that we can generate an appropriate operation name (i.e. free 59 | * of variables) and take a Sampling Decision in case none has been taken so far. 60 | */ 61 | onType("akka.http.scaladsl.server.RequestContextImpl") 62 | .mixin(classOf[HasMatchingContext.Mixin]) 63 | .intercept(method("copy"), RequestContextCopyInterceptor) 64 | 65 | onType("akka.http.scaladsl.server.directives.PathDirectives") 66 | .intercept(method("rawPathPrefix"), classOf[PathDirectivesRawPathPrefixInterceptor]) 67 | 68 | onType("akka.http.scaladsl.server.directives.FutureDirectives") 69 | .intercept(method("onComplete"), classOf[ResolveOperationNameOnRouteInterceptor]) 70 | 71 | onTypes("akka.http.scaladsl.server.directives.OnSuccessMagnet$", "akka.http.scaladsl.server.directives.CompleteOrRecoverWithMagnet$") 72 | .intercept(method("apply"), classOf[ResolveOperationNameOnRouteInterceptor]) 73 | 74 | onType("akka.http.scaladsl.server.directives.RouteDirectives") 75 | .intercept(method("complete"), classOf[ResolveOperationNameOnRouteInterceptor]) 76 | .intercept(method("redirect"), classOf[ResolveOperationNameOnRouteInterceptor]) 77 | .intercept(method("failWith"), classOf[ResolveOperationNameOnRouteInterceptor]) 78 | 79 | 80 | /** 81 | * This allows us to keep the right Context when Futures go through Akka HTTP's FastFuture and transformantions made 82 | * to them. Without this, it might happen that when a Future is already completed and used on any of the Futures 83 | * directives we might get a Context mess up. 84 | */ 85 | onTypes("akka.http.scaladsl.util.FastFuture$FulfilledFuture", "akka.http.scaladsl.util.FastFuture$ErrorFuture") 86 | .mixin(classOf[HasContext.MixinWithInitializer]) 87 | .advise(method("transform"), InvokeWithCapturedContext) 88 | .advise(method("transformWith"), InvokeWithCapturedContext) 89 | .advise(method("onComplete"), InvokeWithCapturedContext) 90 | 91 | onType("akka.http.scaladsl.util.FastFuture$") 92 | .intercept(method("transformWith$extension").and(takesArguments(4)), FastFutureTransformWithAdvice) 93 | } 94 | 95 | trait HasMatchingContext { 96 | def defaultOperationName: String 97 | def matchingContext: Seq[PathMatchingContext] 98 | def setMatchingContext(ctx: Seq[PathMatchingContext]): Unit 99 | def setDefaultOperationName(defaultOperationName: String): Unit 100 | def prependMatchingContext(matched: PathMatchingContext): Unit 101 | } 102 | 103 | object HasMatchingContext { 104 | 105 | case class PathMatchingContext ( 106 | fullPath: String, 107 | matched: Matched[_] 108 | ) 109 | 110 | class Mixin(var matchingContext: Seq[PathMatchingContext], var defaultOperationName: String) extends HasMatchingContext { 111 | 112 | override def setMatchingContext(matchingContext: Seq[PathMatchingContext]): Unit = 113 | this.matchingContext = matchingContext 114 | 115 | override def setDefaultOperationName(defaultOperationName: String): Unit = 116 | this.defaultOperationName = defaultOperationName 117 | 118 | override def prependMatchingContext(matched: PathMatchingContext): Unit = 119 | matchingContext = matched +: matchingContext 120 | 121 | @Initializer 122 | def initialize(): Unit = 123 | matchingContext = Seq.empty 124 | } 125 | } 126 | 127 | class ResolveOperationNameOnRouteInterceptor 128 | object ResolveOperationNameOnRouteInterceptor { 129 | import akka.http.scaladsl.util.FastFuture._ 130 | 131 | // We are replacing some of the basic directives here to ensure that we will resolve both the Sampling Decision and 132 | // the operation name before the request gets to the actual handling code (presumably inside of a "complete" 133 | // directive. 134 | 135 | def complete(m: => ToResponseMarshallable): StandardRoute = 136 | StandardRoute(resolveOperationName(_).complete(m)) 137 | 138 | def redirect(uri: Uri, redirectionType: Redirection): StandardRoute = 139 | StandardRoute(resolveOperationName(_).redirect(uri, redirectionType)) 140 | 141 | def failWith(error: Throwable): StandardRoute = { 142 | Kamon.currentSpan().fail(error) 143 | StandardRoute(resolveOperationName(_).fail(error)) 144 | } 145 | 146 | def onComplete[T](future: => Future[T]): Directive1[Try[T]] = 147 | Directive { inner => ctx => 148 | import ctx.executionContext 149 | resolveOperationName(ctx) 150 | future.fast.transformWith(t => inner(Tuple1(t))(ctx)) 151 | } 152 | 153 | def apply[T](future: => Future[T])(implicit tupler: Tupler[T]): OnSuccessMagnet { type Out = tupler.Out } = 154 | new OnSuccessMagnet { 155 | type Out = tupler.Out 156 | val directive = Directive[tupler.Out] { inner => ctx => 157 | import ctx.executionContext 158 | resolveOperationName(ctx) 159 | future.fast.flatMap(t => inner(tupler(t))(ctx)) 160 | }(tupler.OutIsTuple) 161 | } 162 | 163 | def apply[T](future: => Future[T])(implicit m: ToResponseMarshaller[T]): CompleteOrRecoverWithMagnet = 164 | new CompleteOrRecoverWithMagnet { 165 | val directive = Directive[Tuple1[Throwable]] { inner => ctx => 166 | import ctx.executionContext 167 | resolveOperationName(ctx) 168 | future.fast.transformWith { 169 | case Success(res) => ctx.complete(res) 170 | case Failure(error) => inner(Tuple1(error))(ctx) 171 | } 172 | } 173 | } 174 | 175 | private def resolveOperationName(requestContext: RequestContext): RequestContext = { 176 | 177 | // We will only change the operation name if the last edit made to it was an automatic one. At this point, the only 178 | // way in which the operation name might have changed is if the user changed it with the operationName directive or 179 | // by accessing the Span and changing it directly there, so we wouldn't want to overwrite that. 180 | 181 | Kamon.currentContext().get(LastAutomaticOperationNameEdit.Key).foreach(lastEdit => { 182 | val currentSpan = Kamon.currentSpan() 183 | 184 | if(lastEdit.allowAutomaticChanges) { 185 | if(currentSpan.operationName() == lastEdit.operationName) { 186 | val allMatches = requestContext.asInstanceOf[HasMatchingContext].matchingContext.reverse.map(singleMatch) 187 | val operationName = allMatches.mkString("") 188 | 189 | if (operationName.nonEmpty) { 190 | currentSpan 191 | .name(operationName) 192 | .takeSamplingDecision() 193 | 194 | lastEdit.operationName = operationName 195 | } 196 | } else { 197 | lastEdit.allowAutomaticChanges = false 198 | } 199 | } else { 200 | currentSpan.takeSamplingDecision() 201 | } 202 | }) 203 | 204 | requestContext 205 | } 206 | 207 | private def singleMatch(matching: PathMatchingContext): String = { 208 | val rest = matching.matched.pathRest.toString() 209 | val consumedCount = matching.fullPath.length - rest.length 210 | val consumedSegment = matching.fullPath.substring(0, consumedCount) 211 | 212 | matching.matched.extractions match { 213 | case () => //string segment matched 214 | consumedSegment 215 | case tuple: Product => 216 | val values = tuple.productIterator.toList map { 217 | case Some(x) => List(x.toString) 218 | case None => Nil 219 | case long: Long => List(long.toString, long.toHexString) 220 | case int: Int => List(int.toString, int.toHexString) 221 | case a: Any => List(a.toString) 222 | } 223 | values.flatten.fold(consumedSegment) { (full, value) => 224 | val r = "(?i)(^|/)" + Pattern.quote(value) + "($|/)" 225 | full.replaceFirst(r, "$1{}$2") 226 | } 227 | } 228 | } 229 | } 230 | 231 | /** 232 | * Tracks the last operation name that was automatically assigned to an operation via instrumentation. The 233 | * instrumentation might assign a name to the operations via settings on the HTTP Server instrumentation instance or 234 | * via the Path directives instrumentation, but might never reassign a name if the user somehow assigned their own name 235 | * to the operation. Users chan change operation names by: 236 | * - Using operation mappings via configuration of the HTTP Server. 237 | * - Providing a custom HTTP Operation Name Generator for the server. 238 | * - Using the "operationName" directive. 239 | * - Directly accessing the Span for the current operation and changing the name on it. 240 | * 241 | */ 242 | class LastAutomaticOperationNameEdit( 243 | @volatile var operationName: String, 244 | @volatile var allowAutomaticChanges: Boolean 245 | ) 246 | 247 | object LastAutomaticOperationNameEdit { 248 | val Key = Context.key[Option[LastAutomaticOperationNameEdit]]("laone", None) 249 | 250 | def apply(operationName: String, allowAutomaticChanges: Boolean): LastAutomaticOperationNameEdit = 251 | new LastAutomaticOperationNameEdit(operationName, allowAutomaticChanges) 252 | } 253 | 254 | object RequestContextCopyInterceptor { 255 | 256 | @RuntimeType 257 | def copy(@This context: RequestContext, @SuperCall copyCall: Callable[RequestContext]): RequestContext = { 258 | val copiedRequestContext = copyCall.call() 259 | copiedRequestContext.asInstanceOf[HasMatchingContext].setMatchingContext(context.asInstanceOf[HasMatchingContext].matchingContext) 260 | copiedRequestContext 261 | } 262 | } 263 | 264 | class PathDirectivesRawPathPrefixInterceptor 265 | object PathDirectivesRawPathPrefixInterceptor { 266 | import BasicDirectives._ 267 | 268 | def rawPathPrefix[T](@Argument(0) matcher: PathMatcher[T]): Directive[T] = { 269 | implicit val LIsTuple = matcher.ev 270 | 271 | extract(ctx => { 272 | val fullPath = ctx.unmatchedPath.toString() 273 | val matching = matcher(ctx.unmatchedPath) 274 | matching match { 275 | case m: Matched[_] => 276 | ctx.asInstanceOf[HasMatchingContext].prependMatchingContext(PathMatchingContext(fullPath, m)) 277 | case _ => 278 | } 279 | matching 280 | }).flatMap { 281 | case Matched(rest, values) => tprovide(values) & mapRequestContext(_ withUnmatchedPath rest) 282 | case Unmatched => reject 283 | } 284 | } 285 | } 286 | 287 | object FastFutureTransformWithAdvice { 288 | 289 | @RuntimeType 290 | def transformWith[A, B](@Argument(0) future: Future[A], @Argument(1) s: A => Future[B], @Argument(2) f: Throwable => Future[B], 291 | @Argument(3) ec: ExecutionContext, @SuperCall zuper: Callable[Future[B]]): Future[B] = { 292 | 293 | def strictTransform[T](x: T, f: T => Future[B]) = 294 | try f(x) 295 | catch { case NonFatal(e) => FastFuture.failed(e) } 296 | 297 | // If we get a FulfilledFuture or ErrorFuture, those will have the HasContext mixin but since Scala 2.13 the 298 | // "future" will also have a HasContext mixin (its actually a Transformation instance) and since we can't directly 299 | // access FulfilledFuture or ErrorFuture, we are assuming that if it got to this point, it HasContext and it is not 300 | // a Batchable (i.e. a Transformation) then we can do the same we were doing for previous Scala versions. 301 | // 302 | // When we get a regular Future the path is a bit different because the actual Context is not stored in the Future 303 | // but in the value itself via HasContext instrumentation on scala.util.Try 304 | // 305 | if(future.isInstanceOf[HasContext] && !future.isInstanceOf[Batchable]) 306 | zuper.call() 307 | else { 308 | future.value match { 309 | case None => 310 | val p = Promise[B]() 311 | future.onComplete { 312 | case Success(a) => p completeWith strictTransform(a, s) 313 | case Failure(e) => p completeWith strictTransform(e, f) 314 | }(ec) 315 | p.future 316 | case Some(value) => 317 | // This is possible because of the Future's instrumentation 318 | val futureContext = value.asInstanceOf[HasContext].context 319 | val scope = Kamon.storeContext(futureContext) 320 | 321 | val transformedFuture = value match { 322 | case Success(a) => strictTransform(a, s) 323 | case Failure(e) => strictTransform(e, f) 324 | } 325 | 326 | scope.close() 327 | transformedFuture 328 | } 329 | } 330 | } 331 | } 332 | 333 | 334 | object Http2BlueprintInterceptor { 335 | 336 | case class HandlerWithEndpoint(interface: String, port: Int, handler: HttpRequest => Future[HttpResponse]) 337 | extends (HttpRequest => Future[HttpResponse]) { 338 | 339 | override def apply(request: HttpRequest): Future[HttpResponse] = handler(request) 340 | } 341 | 342 | @RuntimeType 343 | def handleWithStreamIdHeader(@Argument(1) handler: HttpRequest => Future[HttpResponse], 344 | @SuperCall zuper: Callable[Flow[HttpRequest, HttpResponse, NotUsed]]): Flow[HttpRequest, HttpResponse, NotUsed] = { 345 | 346 | handler match { 347 | case HandlerWithEndpoint(interface, port, _) => 348 | ServerFlowWrapper(zuper.call(), interface, port) 349 | 350 | case _ => 351 | zuper.call() 352 | } 353 | } 354 | } --------------------------------------------------------------------------------