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