Skip to content

Commit 3bd752f

Browse files
committed
Add Ktor 3.0 support
1 parent 00de5e3 commit 3bd752f

File tree

8 files changed

+349
-2
lines changed

8 files changed

+349
-2
lines changed

instrumentation/ktor/ktor-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/ktor/v2_0/ServerInstrumentation.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
package io.opentelemetry.javaagent.instrumentation.ktor.v2_0;
77

88
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
9-
import static net.bytebuddy.matcher.ElementMatchers.named;
9+
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
1010

1111
import io.ktor.server.application.Application;
1212
import io.ktor.server.application.ApplicationPluginKt;
@@ -25,7 +25,10 @@
2525
public class ServerInstrumentation implements TypeInstrumentation {
2626
@Override
2727
public ElementMatcher<TypeDescription> typeMatcher() {
28-
return named("io.ktor.server.engine.ApplicationEngineEnvironmentReloading");
28+
return namedOneOf(
29+
"io.ktor.server.engine.ApplicationEngineEnvironmentReloading", // Ktor 2.0
30+
"io.ktor.server.engine.EmbeddedServer" // Ktor 3.0
31+
);
2932
}
3033

3134
@Override
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2+
3+
plugins {
4+
id("otel.java-conventions")
5+
6+
id("org.jetbrains.kotlin.jvm")
7+
}
8+
9+
val ktorVersion = "3.0.0"
10+
11+
dependencies {
12+
api(project(":testing-common"))
13+
14+
implementation("io.ktor:ktor-client-core:$ktorVersion")
15+
implementation("io.ktor:ktor-server-core:$ktorVersion")
16+
17+
implementation("io.opentelemetry:opentelemetry-extension-kotlin")
18+
19+
compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
20+
compileOnly("io.ktor:ktor-server-netty:$ktorVersion")
21+
compileOnly("io.ktor:ktor-client-cio:$ktorVersion")
22+
}
23+
24+
kotlin {
25+
compilerOptions {
26+
jvmTarget.set(JvmTarget.JVM_1_8)
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.ktor.v3_0.client
7+
8+
import io.ktor.client.*
9+
import io.ktor.client.engine.cio.*
10+
import io.ktor.client.plugins.*
11+
import io.ktor.client.request.*
12+
import io.ktor.http.*
13+
import io.opentelemetry.context.Context
14+
import io.opentelemetry.extension.kotlin.asContextElement
15+
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest
16+
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult
17+
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions
18+
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES
19+
import io.opentelemetry.semconv.NetworkAttributes
20+
import kotlinx.coroutines.*
21+
import java.net.URI
22+
23+
abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest<HttpRequestBuilder>() {
24+
25+
private val client = HttpClient(CIO) {
26+
install(HttpRedirect)
27+
28+
installTracing()
29+
}
30+
31+
abstract fun HttpClientConfig<*>.installTracing()
32+
33+
override fun buildRequest(requestMethod: String, uri: URI, requestHeaders: MutableMap<String, String>) = HttpRequestBuilder(uri.toURL()).apply {
34+
method = HttpMethod.parse(requestMethod)
35+
36+
requestHeaders.forEach { (header, value) -> headers.append(header, value) }
37+
}
38+
39+
override fun sendRequest(request: HttpRequestBuilder, method: String, uri: URI, headers: MutableMap<String, String>) = runBlocking {
40+
client.request(request).status.value
41+
}
42+
43+
override fun sendRequestWithCallback(
44+
request: HttpRequestBuilder,
45+
method: String,
46+
uri: URI,
47+
headers: MutableMap<String, String>,
48+
httpClientResult: HttpClientResult,
49+
) {
50+
CoroutineScope(Dispatchers.Default + Context.current().asContextElement()).launch {
51+
try {
52+
val statusCode = client.request(request).status.value
53+
httpClientResult.complete(statusCode)
54+
} catch (e: Throwable) {
55+
httpClientResult.complete(e)
56+
}
57+
}
58+
}
59+
60+
override fun configure(optionsBuilder: HttpClientTestOptions.Builder) {
61+
with(optionsBuilder) {
62+
disableTestReadTimeout()
63+
// this instrumentation creates a span per each physical request
64+
// related issue https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/5722
65+
disableTestRedirects()
66+
spanEndsAfterBody()
67+
68+
setHttpAttributes { DEFAULT_HTTP_ATTRIBUTES - setOf(NetworkAttributes.NETWORK_PROTOCOL_VERSION) }
69+
70+
setSingleConnectionFactory { host, port ->
71+
KtorHttpClientSingleConnection(host, port) { installTracing() }
72+
}
73+
}
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.ktor.v3_0.client
7+
8+
import io.ktor.client.*
9+
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension
10+
import org.junit.jupiter.api.extension.RegisterExtension
11+
12+
class KtorHttpClientInstrumentationTest : AbstractKtorHttpClientTest() {
13+
14+
companion object {
15+
@JvmStatic
16+
@RegisterExtension
17+
private val TESTING = HttpClientInstrumentationExtension.forAgent()
18+
}
19+
20+
override fun HttpClientConfig<*>.installTracing() {
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.ktor.v3_0.client
7+
8+
import io.ktor.client.*
9+
import io.ktor.client.engine.cio.*
10+
import io.ktor.client.request.*
11+
import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection
12+
import kotlinx.coroutines.runBlocking
13+
14+
class KtorHttpClientSingleConnection(
15+
private val host: String,
16+
private val port: Int,
17+
private val installTracing: HttpClientConfig<*>.() -> Unit,
18+
) : SingleConnection {
19+
20+
private val client: HttpClient
21+
22+
init {
23+
val engine = CIO.create {
24+
maxConnectionsCount = 1
25+
}
26+
27+
client = HttpClient(engine) {
28+
installTracing()
29+
}
30+
}
31+
32+
override fun doRequest(path: String, requestHeaders: MutableMap<String, String>) = runBlocking {
33+
val request = HttpRequestBuilder(
34+
scheme = "http",
35+
host = host,
36+
port = port,
37+
path = path,
38+
).apply {
39+
requestHeaders.forEach { (name, value) -> headers.append(name, value) }
40+
}
41+
42+
client.request(request).status.value
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.ktor.v3_0.server
7+
8+
import io.ktor.http.*
9+
import io.ktor.server.application.*
10+
import io.ktor.server.engine.*
11+
import io.ktor.server.netty.*
12+
import io.ktor.server.request.*
13+
import io.ktor.server.response.*
14+
import io.ktor.server.routing.*
15+
import io.opentelemetry.api.trace.Span
16+
import io.opentelemetry.api.trace.SpanKind
17+
import io.opentelemetry.api.trace.StatusCode
18+
import io.opentelemetry.context.Context
19+
import io.opentelemetry.extension.kotlin.asContextElement
20+
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension
21+
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest
22+
import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions
23+
import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint
24+
import io.opentelemetry.semconv.ServerAttributes
25+
import kotlinx.coroutines.withContext
26+
import java.util.concurrent.ExecutionException
27+
import java.util.concurrent.TimeUnit
28+
29+
abstract class AbstractKtorHttpServerTest : AbstractHttpServerTest<EmbeddedServer<*, *>>() {
30+
31+
abstract fun getTesting(): InstrumentationExtension
32+
33+
abstract fun installOpenTelemetry(application: Application)
34+
35+
override fun setupServer(): EmbeddedServer<*, *> {
36+
return embeddedServer(Netty, port = port) {
37+
installOpenTelemetry(this)
38+
39+
routing {
40+
get(ServerEndpoint.SUCCESS.path) {
41+
controller(ServerEndpoint.SUCCESS) {
42+
call.respondText(ServerEndpoint.SUCCESS.body, status = HttpStatusCode.fromValue(ServerEndpoint.SUCCESS.status))
43+
}
44+
}
45+
46+
get(ServerEndpoint.REDIRECT.path) {
47+
controller(ServerEndpoint.REDIRECT) {
48+
call.respondRedirect(ServerEndpoint.REDIRECT.body)
49+
}
50+
}
51+
52+
get(ServerEndpoint.ERROR.path) {
53+
controller(ServerEndpoint.ERROR) {
54+
call.respondText(ServerEndpoint.ERROR.body, status = HttpStatusCode.fromValue(ServerEndpoint.ERROR.status))
55+
}
56+
}
57+
58+
get(ServerEndpoint.EXCEPTION.path) {
59+
controller(ServerEndpoint.EXCEPTION) {
60+
throw Exception(ServerEndpoint.EXCEPTION.body)
61+
}
62+
}
63+
64+
get("/query") {
65+
controller(ServerEndpoint.QUERY_PARAM) {
66+
call.respondText("some=${call.request.queryParameters["some"]}", status = HttpStatusCode.fromValue(ServerEndpoint.QUERY_PARAM.status))
67+
}
68+
}
69+
70+
get("/path/{id}/param") {
71+
controller(ServerEndpoint.PATH_PARAM) {
72+
call.respondText(
73+
call.parameters["id"]
74+
?: "",
75+
status = HttpStatusCode.fromValue(ServerEndpoint.PATH_PARAM.status),
76+
)
77+
}
78+
}
79+
80+
get("/child") {
81+
controller(ServerEndpoint.INDEXED_CHILD) {
82+
ServerEndpoint.INDEXED_CHILD.collectSpanAttributes { call.request.queryParameters[it] }
83+
call.respondText(ServerEndpoint.INDEXED_CHILD.body, status = HttpStatusCode.fromValue(ServerEndpoint.INDEXED_CHILD.status))
84+
}
85+
}
86+
87+
get("/captureHeaders") {
88+
controller(ServerEndpoint.CAPTURE_HEADERS) {
89+
call.response.header("X-Test-Response", call.request.header("X-Test-Request") ?: "")
90+
call.respondText(ServerEndpoint.CAPTURE_HEADERS.body, status = HttpStatusCode.fromValue(ServerEndpoint.CAPTURE_HEADERS.status))
91+
}
92+
}
93+
}
94+
}.start()
95+
}
96+
97+
override fun stopServer(server: EmbeddedServer<*, *>) {
98+
server.stop(0, 10, TimeUnit.SECONDS)
99+
}
100+
101+
// Copy in HttpServerTest.controller but make it a suspending function
102+
private suspend fun controller(endpoint: ServerEndpoint, wrapped: suspend () -> Unit) {
103+
assert(Span.current().spanContext.isValid, { "Controller should have a parent span. " })
104+
if (endpoint == ServerEndpoint.NOT_FOUND) {
105+
wrapped()
106+
}
107+
val span = getTesting().openTelemetry.getTracer("test").spanBuilder("controller").setSpanKind(SpanKind.INTERNAL).startSpan()
108+
try {
109+
withContext(Context.current().with(span).asContextElement()) {
110+
wrapped()
111+
}
112+
span.end()
113+
} catch (e: Exception) {
114+
span.setStatus(StatusCode.ERROR)
115+
span.recordException(if (e is ExecutionException) e.cause ?: e else e)
116+
span.end()
117+
throw e
118+
}
119+
}
120+
121+
override fun configure(options: HttpServerTestOptions) {
122+
options.setTestPathParam(true)
123+
124+
options.setHttpAttributes {
125+
HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES - ServerAttributes.SERVER_PORT
126+
}
127+
128+
options.setExpectedHttpRoute { endpoint, method ->
129+
when (endpoint) {
130+
ServerEndpoint.PATH_PARAM -> "/path/{id}/param"
131+
else -> expectedHttpRoute(endpoint, method)
132+
}
133+
}
134+
135+
// ktor does not have a controller lifecycle so the server span ends immediately when the
136+
// response is sent, which is before the controller span finishes.
137+
options.setVerifyServerSpanEndTime(false)
138+
139+
options.setResponseCodeOnNonStandardHttpMethod(405)
140+
}
141+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.ktor.v3_0.server
7+
8+
import io.ktor.server.application.*
9+
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension
10+
import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension
11+
import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions
12+
import org.junit.jupiter.api.extension.RegisterExtension
13+
14+
class KtorHttpServerInstrumentationTest : AbstractKtorHttpServerTest() {
15+
16+
companion object {
17+
@JvmStatic
18+
@RegisterExtension
19+
val TESTING: InstrumentationExtension = HttpServerInstrumentationExtension.forAgent()
20+
}
21+
22+
override fun getTesting(): InstrumentationExtension {
23+
return TESTING
24+
}
25+
26+
override fun installOpenTelemetry(application: Application) {
27+
}
28+
29+
override fun configure(options: HttpServerTestOptions) {
30+
super.configure(options)
31+
options.setTestException(false)
32+
}
33+
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@ include(":instrumentation:ktor:ktor-1.0:library")
402402
include(":instrumentation:ktor:ktor-2.0:javaagent")
403403
include(":instrumentation:ktor:ktor-2.0:library")
404404
include(":instrumentation:ktor:ktor-2.0:testing")
405+
include(":instrumentation:ktor:ktor-3.0:testing")
405406
include(":instrumentation:ktor:ktor-common:library")
406407
include(":instrumentation:kubernetes-client-7.0:javaagent")
407408
include(":instrumentation:kubernetes-client-7.0:javaagent-unit-tests")

0 commit comments

Comments
 (0)