Skip to content

Commit

Permalink
Add Ktor 3.0 support
Browse files Browse the repository at this point in the history
  • Loading branch information
e5l committed Oct 24, 2024
1 parent 4497fbf commit 94f1fd6
Show file tree
Hide file tree
Showing 11 changed files with 350 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
Comparing source compatibility of opentelemetry-instrumentation-api-2.9.0-SNAPSHOT.jar against opentelemetry-instrumentation-api-2.8.0.jar
Comparing source compatibility of opentelemetry-instrumentation-api-2.9.0-SNAPSHOT.jar against opentelemetry-instrumentation-api-2.9.0.jar
No changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
Comparing source compatibility of opentelemetry-spring-boot-autoconfigure-2.9.0-SNAPSHOT.jar against opentelemetry-spring-boot-autoconfigure-2.8.0.jar
=== UNCHANGED CLASS: PUBLIC io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration (not serializable)
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
*** MODIFIED ANNOTATION: org.springframework.boot.context.properties.EnableConfigurationProperties
*** MODIFIED ELEMENT: value=io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.OtlpExporterProperties,io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.OtelResourceProperties,io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.OtelSpringProperties (<- io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.OtlpExporterProperties,io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.OtelResourceProperties,io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.PropagationProperties)
Comparing source compatibility of opentelemetry-spring-boot-autoconfigure-2.9.0-SNAPSHOT.jar against opentelemetry-spring-boot-autoconfigure-2.9.0.jar
No changes.
2 changes: 1 addition & 1 deletion docs/supported-libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ These are the supported libraries and frameworks:
| [Jodd Http](https://http.jodd.org/) | 4.2+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] |
| [JSP](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/jsp/package-summary.html) | 2.3+ | N/A | Controller Spans [3] |
| [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) | 1.0+ | N/A | Context propagation |
| [Ktor](https://github.com/ktorio/ktor) | 1.0+ | [opentelemetry-ktor-1.0](../instrumentation/ktor/ktor-1.0/library),<br>[opentelemetry-ktor-2.0](../instrumentation/ktor/ktor-2.0/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] |
| [Ktor](https://github.com/ktorio/ktor) | 1.0+ | [opentelemetry-ktor-1.0](../instrumentation/ktor/ktor-1.0/library),<br>[opentelemetry-ktor-2.0](../instrumentation/ktor/ktor-2.0/library) (supports Ktor 3.*) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] |
| [Kubernetes Client](https://github.com/kubernetes-client/java) | 7.0+ | N/A | [HTTP Client Spans] |
| [Lettuce](https://github.com/lettuce-io/lettuce-core) | 4.0+ | [opentelemetry-lettuce-5.1](../instrumentation/lettuce/lettuce-5.1/library) | [Database Client Spans] |
| [Log4j 1](https://logging.apache.org/log4j/1.2/) | 1.2+ | N/A | none |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
public class ServerInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("io.ktor.server.engine.ApplicationEngineEnvironmentReloading");
return named("io.ktor.server.engine.ApplicationEngineEnvironmentReloading")
.or(named("io.ktor.server.engine.EmbeddedServer"));
}

@Override
Expand Down
28 changes: 28 additions & 0 deletions instrumentation/ktor/ktor-3.0/testing/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
id("otel.java-conventions")

id("org.jetbrains.kotlin.jvm")
}

val ktorVersion = "3.0.0"

dependencies {
api(project(":testing-common"))

implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-server-core:$ktorVersion")

implementation("io.opentelemetry:opentelemetry-extension-kotlin")

compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
compileOnly("io.ktor:ktor-server-netty:$ktorVersion")
compileOnly("io.ktor:ktor-client-cio:$ktorVersion")
}

kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_1_8)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.v3_0.client

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.opentelemetry.context.Context
import io.opentelemetry.extension.kotlin.asContextElement
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES
import io.opentelemetry.semconv.NetworkAttributes
import kotlinx.coroutines.*
import java.net.URI

abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest<HttpRequestBuilder>() {

private val client = HttpClient(CIO) {
install(HttpRedirect)

installTracing()
}

abstract fun HttpClientConfig<*>.installTracing()

override fun buildRequest(requestMethod: String, uri: URI, requestHeaders: MutableMap<String, String>) = HttpRequestBuilder(uri.toURL()).apply {
method = HttpMethod.parse(requestMethod)

requestHeaders.forEach { (header, value) -> headers.append(header, value) }
}

override fun sendRequest(request: HttpRequestBuilder, method: String, uri: URI, headers: MutableMap<String, String>) = runBlocking {
client.request(request).status.value
}

override fun sendRequestWithCallback(
request: HttpRequestBuilder,
method: String,
uri: URI,
headers: MutableMap<String, String>,
httpClientResult: HttpClientResult,
) {
CoroutineScope(Dispatchers.Default + Context.current().asContextElement()).launch {
try {
val statusCode = client.request(request).status.value
httpClientResult.complete(statusCode)
} catch (e: Throwable) {
httpClientResult.complete(e)
}
}
}

override fun configure(optionsBuilder: HttpClientTestOptions.Builder) {
with(optionsBuilder) {
disableTestReadTimeout()
// this instrumentation creates a span per each physical request
// related issue https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/5722
disableTestRedirects()
spanEndsAfterBody()

setHttpAttributes { DEFAULT_HTTP_ATTRIBUTES - setOf(NetworkAttributes.NETWORK_PROTOCOL_VERSION) }

setSingleConnectionFactory { host, port ->
KtorHttpClientSingleConnection(host, port) { installTracing() }
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.v3_0.client

import io.ktor.client.*
import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension
import org.junit.jupiter.api.extension.RegisterExtension

class KtorHttpClientInstrumentationTest : AbstractKtorHttpClientTest() {

companion object {
@JvmStatic
@RegisterExtension
private val TESTING = HttpClientInstrumentationExtension.forAgent()
}

override fun HttpClientConfig<*>.installTracing() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.v3_0.client

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection
import kotlinx.coroutines.runBlocking

class KtorHttpClientSingleConnection(
private val host: String,
private val port: Int,
private val installTracing: HttpClientConfig<*>.() -> Unit,
) : SingleConnection {

private val client: HttpClient

init {
val engine = CIO.create {
maxConnectionsCount = 1
}

client = HttpClient(engine) {
installTracing()
}
}

override fun doRequest(path: String, requestHeaders: MutableMap<String, String>) = runBlocking {
val request = HttpRequestBuilder(
scheme = "http",
host = host,
port = port,
path = path,
).apply {
requestHeaders.forEach { (name, value) -> headers.append(name, value) }
}

client.request(request).status.value
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.v3_0.server

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.SpanKind
import io.opentelemetry.api.trace.StatusCode
import io.opentelemetry.context.Context
import io.opentelemetry.extension.kotlin.asContextElement
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension
import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest
import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions
import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint
import io.opentelemetry.semconv.ServerAttributes
import kotlinx.coroutines.withContext
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit

abstract class AbstractKtorHttpServerTest : AbstractHttpServerTest<EmbeddedServer<*, *>>() {

abstract fun getTesting(): InstrumentationExtension

abstract fun installOpenTelemetry(application: Application)

override fun setupServer(): EmbeddedServer<*, *> {
return embeddedServer(Netty, port = port) {
installOpenTelemetry(this)

routing {
get(ServerEndpoint.SUCCESS.path) {
controller(ServerEndpoint.SUCCESS) {
call.respondText(ServerEndpoint.SUCCESS.body, status = HttpStatusCode.fromValue(ServerEndpoint.SUCCESS.status))
}
}

get(ServerEndpoint.REDIRECT.path) {
controller(ServerEndpoint.REDIRECT) {
call.respondRedirect(ServerEndpoint.REDIRECT.body)
}
}

get(ServerEndpoint.ERROR.path) {
controller(ServerEndpoint.ERROR) {
call.respondText(ServerEndpoint.ERROR.body, status = HttpStatusCode.fromValue(ServerEndpoint.ERROR.status))
}
}

get(ServerEndpoint.EXCEPTION.path) {
controller(ServerEndpoint.EXCEPTION) {
throw Exception(ServerEndpoint.EXCEPTION.body)
}
}

get("/query") {
controller(ServerEndpoint.QUERY_PARAM) {
call.respondText("some=${call.request.queryParameters["some"]}", status = HttpStatusCode.fromValue(ServerEndpoint.QUERY_PARAM.status))
}
}

get("/path/{id}/param") {
controller(ServerEndpoint.PATH_PARAM) {
call.respondText(
call.parameters["id"]
?: "",
status = HttpStatusCode.fromValue(ServerEndpoint.PATH_PARAM.status),
)
}
}

get("/child") {
controller(ServerEndpoint.INDEXED_CHILD) {
ServerEndpoint.INDEXED_CHILD.collectSpanAttributes { call.request.queryParameters[it] }
call.respondText(ServerEndpoint.INDEXED_CHILD.body, status = HttpStatusCode.fromValue(ServerEndpoint.INDEXED_CHILD.status))
}
}

get("/captureHeaders") {
controller(ServerEndpoint.CAPTURE_HEADERS) {
call.response.header("X-Test-Response", call.request.header("X-Test-Request") ?: "")
call.respondText(ServerEndpoint.CAPTURE_HEADERS.body, status = HttpStatusCode.fromValue(ServerEndpoint.CAPTURE_HEADERS.status))
}
}
}
}.start()
}

override fun stopServer(server: EmbeddedServer<*, *>) {
server.stop(0, 10, TimeUnit.SECONDS)
}

// Copy in HttpServerTest.controller but make it a suspending function
private suspend fun controller(endpoint: ServerEndpoint, wrapped: suspend () -> Unit) {
assert(Span.current().spanContext.isValid, { "Controller should have a parent span. " })
if (endpoint == ServerEndpoint.NOT_FOUND) {
wrapped()
}
val span = getTesting().openTelemetry.getTracer("test").spanBuilder("controller").setSpanKind(SpanKind.INTERNAL).startSpan()
try {
withContext(Context.current().with(span).asContextElement()) {
wrapped()
}
span.end()
} catch (e: Exception) {
span.setStatus(StatusCode.ERROR)
span.recordException(if (e is ExecutionException) e.cause ?: e else e)
span.end()
throw e
}
}

override fun configure(options: HttpServerTestOptions) {
options.setTestPathParam(true)

options.setHttpAttributes {
HttpServerTestOptions.DEFAULT_HTTP_ATTRIBUTES - ServerAttributes.SERVER_PORT
}

options.setExpectedHttpRoute { endpoint, method ->
when (endpoint) {
ServerEndpoint.PATH_PARAM -> "/path/{id}/param"
else -> expectedHttpRoute(endpoint, method)
}
}

// ktor does not have a controller lifecycle so the server span ends immediately when the
// response is sent, which is before the controller span finishes.
options.setVerifyServerSpanEndTime(false)

options.setResponseCodeOnNonStandardHttpMethod(405)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.v3_0.server

import io.ktor.server.application.*
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension
import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension
import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions
import org.junit.jupiter.api.extension.RegisterExtension

class KtorHttpServerInstrumentationTest : AbstractKtorHttpServerTest() {

companion object {
@JvmStatic
@RegisterExtension
val TESTING: InstrumentationExtension = HttpServerInstrumentationExtension.forAgent()
}

override fun getTesting(): InstrumentationExtension {
return TESTING
}

override fun installOpenTelemetry(application: Application) {
}

override fun configure(options: HttpServerTestOptions) {
super.configure(options)
options.setTestException(false)
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ include(":instrumentation:ktor:ktor-1.0:library")
include(":instrumentation:ktor:ktor-2.0:javaagent")
include(":instrumentation:ktor:ktor-2.0:library")
include(":instrumentation:ktor:ktor-2.0:testing")
include(":instrumentation:ktor:ktor-3.0:testing")
include(":instrumentation:ktor:ktor-common:library")
include(":instrumentation:kubernetes-client-7.0:javaagent")
include(":instrumentation:kubernetes-client-7.0:javaagent-unit-tests")
Expand Down

0 comments on commit 94f1fd6

Please sign in to comment.