Skip to content

Commit afd6c82

Browse files
committed
instrument HTTP/1 and HTTP/2 requests on the same server
1 parent 63cf15c commit afd6c82

File tree

7 files changed

+96
-26
lines changed

7 files changed

+96
-26
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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 FlowOpsMapAsyncAdvice {
10+
11+
public static class EndpointInfo {
12+
public final String listenInterface;
13+
public final int listenPort;
14+
15+
public EndpointInfo(String listenInterface, int listenPort) {
16+
this.listenInterface = listenInterface;
17+
this.listenPort = listenPort;
18+
}
19+
}
20+
21+
public static ThreadLocal<EndpointInfo> currentEndpoint = new ThreadLocal<>();
22+
23+
@Advice.OnMethodExit
24+
public static void onExit(@Advice.Return(readOnly = false) akka.stream.scaladsl.FlowOps returnedFlow) {
25+
EndpointInfo bindAndHandlerEndpoint = currentEndpoint.get();
26+
27+
if(bindAndHandlerEndpoint != null) {
28+
returnedFlow = ServerFlowWrapper.apply(
29+
(Flow<HttpRequest, HttpResponse, NotUsed>) returnedFlow,
30+
bindAndHandlerEndpoint.listenInterface,
31+
bindAndHandlerEndpoint.listenPort
32+
);
33+
}
34+
}
35+
}

instrumentation/kamon-akka-http/src/main/java/kamon/instrumentation/akka/http/Http2ExtBindAndHandleAdvice.java

+6
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ public static void onEnter(@Advice.Argument(value = 0, readOnly = false) Functio
2929
@Advice.Argument(1) String iface,
3030
@Advice.Argument(2) Integer port) {
3131

32+
FlowOpsMapAsyncAdvice.currentEndpoint.set(new FlowOpsMapAsyncAdvice.EndpointInfo(iface, port));
3233
handler = new Http2BlueprintInterceptor.HandlerWithEndpoint(iface, port, handler);
3334
}
35+
36+
@Advice.OnMethodExit
37+
public static void onExit() {
38+
FlowOpsMapAsyncAdvice.currentEndpoint.remove();
39+
}
3440
}

instrumentation/kamon-akka-http/src/main/resources/reference.conf

+3-1
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,9 @@ kanela.modules {
245245

246246
within = [
247247
"akka.http.*",
248-
"akka.grpc.internal.*"
248+
"akka.grpc.internal.*",
249+
"akka.stream.scaladsl.Flow",
250+
"akka.stream.scaladsl.FlowOps"
249251
]
250252
}
251253
}

instrumentation/kamon-akka-http/src/main/scala-2.11/kamon/instrumentation/akka/http/AkkaHttpServerInstrumentation.scala

+7
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ class AkkaHttpServerInstrumentation extends InstrumentationBuilder {
7777
.intercept(method("redirect"), classOf[ResolveOperationNameOnRouteInterceptor])
7878
.intercept(method("failWith"), classOf[ResolveOperationNameOnRouteInterceptor])
7979

80+
/**
81+
* Support for HTTP/1 and HTTP/2 at the same time.
82+
*/
83+
84+
onType("akka.stream.scaladsl.Flow")
85+
.advise(method("mapAsync"), classOf[FlowOpsMapAsyncAdvice])
86+
8087
}
8188

8289
trait HasMatchingContext {

instrumentation/kamon-akka-http/src/main/scala-2.12/kamon/instrumentation/akka/http/AkkaHttpServerInstrumentation.scala

+8-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ import akka.NotUsed
4141
import akka.http.scaladsl.server.RouteResult.Rejected
4242
import akka.stream.scaladsl.Flow
4343
import kamon.context.Context
44-
import kanela.agent.libs.net.bytebuddy.asm.Advice
4544
import kanela.agent.libs.net.bytebuddy.matcher.ElementMatchers.isPublic
4645

4746
import scala.collection.immutable
@@ -103,6 +102,14 @@ class AkkaHttpServerInstrumentation extends InstrumentationBuilder {
103102
onType("akka.http.scaladsl.Http2Ext")
104103
.advise(method("bindAndHandleAsync") and isPublic(), classOf[Http2ExtBindAndHandleAdvice])
105104

105+
106+
/**
107+
* Support for HTTP/1 and HTTP/2 at the same time.
108+
*
109+
*/
110+
111+
onType("akka.stream.scaladsl.FlowOps")
112+
.advise(method("mapAsync"), classOf[FlowOpsMapAsyncAdvice])
106113
}
107114

108115
trait HasMatchingContext {

instrumentation/kamon-akka-http/src/main/scala-2.13/kamon/instrumentation/akka/http/AkkaHttpServerInstrumentation.scala

+7-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import akka.NotUsed
2525
import akka.http.scaladsl.server.RouteResult.Rejected
2626
import akka.stream.scaladsl.Flow
2727
import kamon.context.Context
28-
import kanela.agent.libs.net.bytebuddy.asm.Advice
2928
import kanela.agent.libs.net.bytebuddy.matcher.ElementMatchers.isPublic
3029

3130
import scala.collection.immutable
@@ -86,6 +85,13 @@ class AkkaHttpServerInstrumentation extends InstrumentationBuilder {
8685
onType("akka.http.scaladsl.Http2Ext")
8786
.advise(method("bindAndHandleAsync") and isPublic(), classOf[Http2ExtBindAndHandleAdvice])
8887

88+
/**
89+
* Support for HTTP/1 and HTTP/2 at the same time.
90+
*
91+
*/
92+
93+
onType("akka.stream.scaladsl.FlowOps")
94+
.advise(method("mapAsync"), classOf[FlowOpsMapAsyncAdvice])
8995
}
9096

9197
trait HasMatchingContext {

instrumentation/kamon-akka-http/src/test/scala/kamon/akka/http/AkkaHttpServerTracingSpec.scala

+30-23
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ class AkkaHttpServerTracingSpec extends AnyWordSpecLike with Matchers with Scala
4343

4444
val (sslSocketFactory, trustManager) = clientSSL()
4545
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 okHttp1ONly = new OkHttpClient.Builder()
4651
.sslSocketFactory(sslSocketFactory, trustManager)
4752
.protocols(List(Protocol.HTTP_1_1).asJava)
4853
.hostnameVerifier(new HostnameVerifier { override def verify(s: String, sslSession: SSLSession): Boolean = true })
@@ -53,22 +58,24 @@ class AkkaHttpServerTracingSpec extends AnyWordSpecLike with Matchers with Scala
5358
val httpWebServer = startServer(interface, 8081, https = false)
5459
val httpsWebServer = startServer(interface, 8082, https = true)
5560

56-
testSuite("HTTP", httpWebServer)
57-
testSuite("HTTPS", httpsWebServer)
61+
testSuite("HTTP", httpWebServer, okHttp)
62+
testSuite("HTTPS", httpsWebServer, okHttp)
63+
testSuite("HTTPS with HTTP/1 only clients", httpsWebServer, okHttp1ONly)
5864

59-
def testSuite(httpVersion: String, server: WebServer) = {
65+
def testSuite(httpVersion: String, server: WebServer, client: OkHttpClient) = {
6066
val interface = server.interface
6167
val port = server.port
6268
val protocol = server.protocol
6369

6470
s"the Akka HTTP server instrumentation with ${httpVersion}" should {
6571
"create a server Span when receiving requests" in {
6672
val target = s"$protocol://$interface:$port/$dummyPathOk"
67-
okHttp.newCall(new Request.Builder().url(target).build()).execute()
73+
client.newCall(new Request.Builder().url(target).build()).execute()
74+
6875

6976
eventually(timeout(10 seconds)) {
7077
val span = testSpanReporter().nextSpan().value
71-
span.tags.get(plain("http.url")) shouldBe target
78+
span.tags.get(plain("http.url")) should endWith(s"$interface:$port/$dummyPathOk")
7279
span.metricTags.get(plain("component")) shouldBe "akka.http.server"
7380
span.metricTags.get(plain("http.method")) shouldBe "GET"
7481
span.metricTags.get(plainLong("http.status_code")) shouldBe 200L
@@ -78,7 +85,7 @@ class AkkaHttpServerTracingSpec extends AnyWordSpecLike with Matchers with Scala
7885
"return the correct operation name with overloaded route" in {
7986
val target = s"$protocol://$interface:$port/some_endpoint"
8087

81-
okHttp.newCall(new Request.Builder()
88+
client.newCall(new Request.Builder()
8289
.get()
8390
.url(target).build())
8491
.execute()
@@ -91,7 +98,7 @@ class AkkaHttpServerTracingSpec extends AnyWordSpecLike with Matchers with Scala
9198
val path = s"extraction/nested/42/fixed/anchor/32/${UUID.randomUUID().toString}/fixed/44/CafE"
9299
val expected = "/extraction/nested/{}/fixed/anchor/{}/{}/fixed/{}/{}"
93100
val target = s"$protocol://$interface:$port/$path"
94-
okHttp.newCall(new Request.Builder().url(target).build()).execute()
101+
client.newCall(new Request.Builder().url(target).build()).execute()
95102

96103
eventually(timeout(10 seconds)) {
97104
val span = testSpanReporter().nextSpan().value
@@ -103,7 +110,7 @@ class AkkaHttpServerTracingSpec extends AnyWordSpecLike with Matchers with Scala
103110
val path = "extraction/segment/special**"
104111
val expected = "/extraction/segment/{}"
105112
val target = s"$protocol://$interface:$port/$path"
106-
val response = okHttp.newCall(new Request.Builder().url(target).build()).execute()
113+
val response = client.newCall(new Request.Builder().url(target).build()).execute()
107114

108115
response.code() shouldBe 200
109116
response.body().string() shouldBe "special**"
@@ -118,7 +125,7 @@ class AkkaHttpServerTracingSpec extends AnyWordSpecLike with Matchers with Scala
118125
val path = "extraction/on-complete/42/more-path"
119126
val expected = "/extraction/on-complete/{}/more-path"
120127
val target = s"$protocol://$interface:$port/$path"
121-
okHttp.newCall(new Request.Builder().url(target).build()).execute()
128+
client.newCall(new Request.Builder().url(target).build()).execute()
122129

123130
eventually(timeout(10 seconds)) {
124131
val span = testSpanReporter().nextSpan().value
@@ -130,7 +137,7 @@ class AkkaHttpServerTracingSpec extends AnyWordSpecLike with Matchers with Scala
130137
val path = "extraction/on-success/42/after"
131138
val expected = "/extraction/on-success/{}/after"
132139
val target = s"$protocol://$interface:$port/$path"
133-
okHttp.newCall(new Request.Builder().url(target).build()).execute()
140+
client.newCall(new Request.Builder().url(target).build()).execute()
134141

135142
eventually(timeout(10 seconds)) {
136143
val span = testSpanReporter().nextSpan().value
@@ -142,7 +149,7 @@ class AkkaHttpServerTracingSpec extends AnyWordSpecLike with Matchers with Scala
142149
val path = "extraction/complete-or-recover-with/42/after"
143150
val expected = "/extraction/complete-or-recover-with/{}/after"
144151
val target = s"$protocol://$interface:$port/$path"
145-
okHttp.newCall(new Request.Builder().url(target).build()).execute()
152+
client.newCall(new Request.Builder().url(target).build()).execute()
146153

147154
eventually(timeout(10 seconds)) {
148155
val span = testSpanReporter().nextSpan().value
@@ -154,7 +161,7 @@ class AkkaHttpServerTracingSpec extends AnyWordSpecLike with Matchers with Scala
154161
val path = "extraction/complete-or-recover-with-success/42/after"
155162
val expected = "/extraction/complete-or-recover-with-success/{}"
156163
val target = s"$protocol://$interface:$port/$path"
157-
okHttp.newCall(new Request.Builder().url(target).build()).execute()
164+
client.newCall(new Request.Builder().url(target).build()).execute()
158165

159166
eventually(timeout(10 seconds)) {
160167
val span = testSpanReporter().nextSpan().value
@@ -166,7 +173,7 @@ class AkkaHttpServerTracingSpec extends AnyWordSpecLike with Matchers with Scala
166173
val path = s"v3/user/3/post/3"
167174
val expected = "/v3/user/{}/post/{}"
168175
val target = s"$protocol://$interface:$port/$path"
169-
okHttp.newCall(new Request.Builder().url(target).build()).execute()
176+
client.newCall(new Request.Builder().url(target).build()).execute()
170177

171178
eventually(timeout(10 seconds)) {
172179
val span = testSpanReporter().nextSpan().value
@@ -177,12 +184,12 @@ class AkkaHttpServerTracingSpec extends AnyWordSpecLike with Matchers with Scala
177184

178185
"change the Span operation name when using the operationName directive" in {
179186
val target = s"$protocol://$interface:$port/$traceOk"
180-
okHttp.newCall(new Request.Builder().url(target).build()).execute()
187+
client.newCall(new Request.Builder().url(target).build()).execute()
181188

182189
eventually(timeout(10 seconds)) {
183190
val span = testSpanReporter().nextSpan().value
184191
span.operationName shouldBe "user-supplied-operation"
185-
span.tags.get(plain("http.url")) shouldBe target
192+
span.tags.get(plain("http.url")) should endWith(s"$interface:$port/$traceOk")
186193
span.metricTags.get(plain("component")) shouldBe "akka.http.server"
187194
span.metricTags.get(plain("http.method")) shouldBe "GET"
188195
span.metricTags.get(plainLong("http.status_code")) shouldBe 200L
@@ -191,12 +198,12 @@ class AkkaHttpServerTracingSpec extends AnyWordSpecLike with Matchers with Scala
191198

192199
"mark spans as failed when request fails" in {
193200
val target = s"$protocol://$interface:$port/$dummyPathError"
194-
okHttp.newCall(new Request.Builder().url(target).build()).execute()
201+
client.newCall(new Request.Builder().url(target).build()).execute()
195202

196203
eventually(timeout(10 seconds)) {
197204
val span = testSpanReporter().nextSpan().value
198205
span.operationName shouldBe s"/$dummyPathError"
199-
span.tags.get(plain("http.url")) shouldBe target
206+
span.tags.get(plain("http.url")) should endWith(s"$interface:$port/$dummyPathError")
200207
span.metricTags.get(plain("component")) shouldBe "akka.http.server"
201208
span.metricTags.get(plain("http.method")) shouldBe "GET"
202209
span.metricTags.get(plainBoolean("error")) shouldBe true
@@ -206,12 +213,12 @@ class AkkaHttpServerTracingSpec extends AnyWordSpecLike with Matchers with Scala
206213

207214
"change the operation name to 'unhandled' when the response status code is 404" in {
208215
val target = s"$protocol://$interface:$port/unknown-path"
209-
okHttp.newCall(new Request.Builder().url(target).build()).execute()
216+
client.newCall(new Request.Builder().url(target).build()).execute()
210217

211218
eventually(timeout(10 seconds)) {
212219
val span = testSpanReporter().nextSpan().value
213220
span.operationName shouldBe "unhandled"
214-
span.tags.get(plain("http.url")) shouldBe target
221+
span.tags.get(plain("http.url")) should endWith(s"$interface:$port/unknown-path")
215222
span.metricTags.get(plain("component")) shouldBe "akka.http.server"
216223
span.metricTags.get(plain("http.method")) shouldBe "GET"
217224
span.metricTags.get(plainBoolean("error")) shouldBe false
@@ -221,7 +228,7 @@ class AkkaHttpServerTracingSpec extends AnyWordSpecLike with Matchers with Scala
221228

222229
"correctly time entity transfer timings" in {
223230
val target = s"$protocol://$interface:$port/$stream"
224-
okHttp.newCall(new Request.Builder().url(target).build()).execute()
231+
client.newCall(new Request.Builder().url(target).build()).execute()
225232

226233
val span = eventually(timeout(10 seconds)) {
227234
val span = testSpanReporter().nextSpan().value
@@ -233,14 +240,14 @@ class AkkaHttpServerTracingSpec extends AnyWordSpecLike with Matchers with Scala
233240
case List(_ @ Mark(_, "http.response.ready")) =>
234241
}
235242

236-
span.tags.get(plain("http.url")) shouldBe target
243+
span.tags.get(plain("http.url")) should endWith(s"$interface:$port/$stream")
237244
span.metricTags.get(plain("component")) shouldBe "akka.http.server"
238245
span.metricTags.get(plain("http.method")) shouldBe "GET"
239246
}
240247

241248
"include the trace-id and keep all user-provided headers in the responses" in {
242249
val target = s"$protocol://$interface:$port/extra-header"
243-
val response = okHttp.newCall(new Request.Builder().url(target).build()).execute()
250+
val response = client.newCall(new Request.Builder().url(target).build()).execute()
244251

245252
response.headers().names() should contain allOf (
246253
"trace-id",
@@ -250,7 +257,7 @@ class AkkaHttpServerTracingSpec extends AnyWordSpecLike with Matchers with Scala
250257

251258
"keep operation names provided by the HTTP Server instrumentation" in {
252259
val target = s"$protocol://$interface:$port/name-will-be-changed"
253-
okHttp.newCall(new Request.Builder().url(target).build()).execute()
260+
client.newCall(new Request.Builder().url(target).build()).execute()
254261

255262
eventually(timeout(10 seconds)) {
256263
val span = testSpanReporter().nextSpan().value

0 commit comments

Comments
 (0)