Add ktor 3 instrumentation (#12562)

This commit is contained in:
Lauri Tulmin 2024-11-11 13:03:11 +02:00 committed by GitHub
parent fdfb764c76
commit 76c2d2d8f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 1578 additions and 586 deletions

View File

@ -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),<br>[opentelemetry-ktor-3.0](../instrumentation/ktor/ktor-3.0/library) | [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 |

View File

@ -1,4 +1,4 @@
# Library Instrumentation for Ktor versions 1.x
# Library Instrumentation for Ktor version 1.x
This package contains libraries to help instrument Ktor. Currently, only server instrumentation is supported.

View File

@ -0,0 +1,22 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
plugins {
id("otel.library-instrumentation")
id("org.jetbrains.kotlin.jvm")
}
dependencies {
implementation(project(":instrumentation:ktor:ktor-common:library"))
implementation("io.opentelemetry:opentelemetry-extension-kotlin")
compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
compileOnly("io.ktor:ktor-client-core:2.0.0")
compileOnly("io.ktor:ktor-server-core:2.0.0")
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_1_8)
@Suppress("deprecation")
languageVersion.set(KotlinVersion.KOTLIN_1_4)
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ktor.client
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.opentelemetry.context.Context
import io.opentelemetry.context.propagation.ContextPropagators
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
abstract class AbstractKtorClientTracing(
private val instrumenter: Instrumenter<HttpRequestData, HttpResponse>,
private val propagators: ContextPropagators,
) {
internal fun createSpan(requestBuilder: HttpRequestBuilder): Context? {
val parentContext = Context.current()
val requestData = requestBuilder.build()
return if (instrumenter.shouldStart(parentContext, requestData)) {
instrumenter.start(parentContext, requestData)
} else {
null
}
}
internal fun populateRequestHeaders(requestBuilder: HttpRequestBuilder, context: Context) {
propagators.textMapPropagator.inject(context, requestBuilder, KtorHttpHeadersSetter)
}
internal fun endSpan(context: Context, call: HttpClientCall, error: Throwable?) {
endSpan(context, HttpRequestBuilder().takeFrom(call.request), call.response, error)
}
internal fun endSpan(context: Context, requestBuilder: HttpRequestBuilder, response: HttpResponse?, error: Throwable?) {
instrumenter.end(context, requestBuilder.build(), response, error)
}
}

View File

@ -0,0 +1,172 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ktor.client
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.opentelemetry.api.OpenTelemetry
import io.opentelemetry.api.common.AttributesBuilder
import io.opentelemetry.context.Context
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor
import io.opentelemetry.instrumentation.ktor.internal.KtorBuilderUtil
abstract class AbstractKtorClientTracingBuilder(
private val instrumentationName: String
) {
companion object {
init {
KtorBuilderUtil.clientBuilderExtractor = { it.clientBuilder }
}
}
internal lateinit var openTelemetry: OpenTelemetry
protected lateinit var clientBuilder: DefaultHttpClientInstrumenterBuilder<HttpRequestData, HttpResponse>
fun setOpenTelemetry(openTelemetry: OpenTelemetry) {
this.openTelemetry = openTelemetry
this.clientBuilder = DefaultHttpClientInstrumenterBuilder.create(
instrumentationName,
openTelemetry,
KtorHttpClientAttributesGetter
)
}
protected fun getOpenTelemetry(): OpenTelemetry {
return openTelemetry
}
@Deprecated(
"Please use method `capturedRequestHeaders`",
ReplaceWith("capturedRequestHeaders(headers.asIterable())")
)
fun setCapturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())
@Deprecated(
"Please use method `capturedRequestHeaders`",
ReplaceWith("capturedRequestHeaders(headers)")
)
fun setCapturedRequestHeaders(headers: List<String>) = capturedRequestHeaders(headers)
fun capturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())
fun capturedRequestHeaders(headers: Iterable<String>) {
clientBuilder.setCapturedRequestHeaders(headers.toList())
}
@Deprecated(
"Please use method `capturedResponseHeaders`",
ReplaceWith("capturedResponseHeaders(headers.asIterable())")
)
fun setCapturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())
@Deprecated(
"Please use method `capturedResponseHeaders`",
ReplaceWith("capturedResponseHeaders(headers)")
)
fun setCapturedResponseHeaders(headers: List<String>) = capturedResponseHeaders(headers)
fun capturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())
fun capturedResponseHeaders(headers: Iterable<String>) {
clientBuilder.setCapturedResponseHeaders(headers.toList())
}
@Deprecated(
"Please use method `knownMethods`",
ReplaceWith("knownMethods(knownMethods)")
)
fun setKnownMethods(knownMethods: Set<String>) = knownMethods(knownMethods)
fun knownMethods(vararg methods: String) = knownMethods(methods.asIterable())
fun knownMethods(vararg methods: HttpMethod) = knownMethods(methods.asIterable())
@JvmName("knownMethodsJvm")
fun knownMethods(methods: Iterable<HttpMethod>) = knownMethods(methods.map { it.value })
fun knownMethods(methods: Iterable<String>) {
clientBuilder.setKnownMethods(methods.toSet())
}
@Deprecated("Please use method `attributeExtractor`")
fun addAttributesExtractors(vararg extractors: AttributesExtractor<in HttpRequestData, in HttpResponse>) = addAttributesExtractors(extractors.asList())
@Deprecated("Please use method `attributeExtractor`")
fun addAttributesExtractors(extractors: Iterable<AttributesExtractor<in HttpRequestData, in HttpResponse>>) {
extractors.forEach {
attributeExtractor {
onStart { it.onStart(attributes, parentContext, request) }
onEnd { it.onEnd(attributes, parentContext, request, response, error) }
}
}
}
fun attributeExtractor(extractorBuilder: ExtractorBuilder.() -> Unit = {}) {
val builder = ExtractorBuilder().apply(extractorBuilder).build()
this.clientBuilder.addAttributeExtractor(
object : AttributesExtractor<HttpRequestData, HttpResponse> {
override fun onStart(attributes: AttributesBuilder, parentContext: Context, request: HttpRequestData) {
builder.onStart(OnStartData(attributes, parentContext, request))
}
override fun onEnd(attributes: AttributesBuilder, context: Context, request: HttpRequestData, response: HttpResponse?, error: Throwable?) {
builder.onEnd(OnEndData(attributes, context, request, response, error))
}
}
)
}
class ExtractorBuilder {
private var onStart: OnStartData.() -> Unit = {}
private var onEnd: OnEndData.() -> Unit = {}
fun onStart(block: OnStartData.() -> Unit) {
onStart = block
}
fun onEnd(block: OnEndData.() -> Unit) {
onEnd = block
}
internal fun build(): Extractor {
return Extractor(onStart, onEnd)
}
}
internal class Extractor(val onStart: OnStartData.() -> Unit, val onEnd: OnEndData.() -> Unit)
data class OnStartData(
val attributes: AttributesBuilder,
val parentContext: Context,
val request: HttpRequestData
)
data class OnEndData(
val attributes: AttributesBuilder,
val parentContext: Context,
val request: HttpRequestData,
val response: HttpResponse?,
val error: Throwable?
)
/**
* Configures the instrumentation to emit experimental HTTP client metrics.
*
* @param emitExperimentalHttpClientMetrics `true` if the experimental HTTP client metrics are to be emitted.
*/
@Deprecated("Please use method `emitExperimentalHttpClientMetrics`")
fun setEmitExperimentalHttpClientMetrics(emitExperimentalHttpClientMetrics: Boolean) {
if (emitExperimentalHttpClientMetrics) {
emitExperimentalHttpClientMetrics()
}
}
fun emitExperimentalHttpClientMetrics() {
clientBuilder.setEmitExperimentalHttpClientMetrics(true)
}
}

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ktor.v2_0.client
package io.opentelemetry.instrumentation.ktor.client
import io.ktor.client.request.*
import io.ktor.client.statement.*

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ktor.v2_0.client
package io.opentelemetry.instrumentation.ktor.client
import io.ktor.client.request.HttpRequestBuilder
import io.opentelemetry.context.propagation.TextMapSetter

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ktor.v2_0.internal
package io.opentelemetry.instrumentation.ktor.internal
import io.ktor.client.request.*
import io.ktor.client.statement.*
@ -11,14 +11,16 @@ import io.ktor.server.request.*
import io.ktor.server.response.*
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder
import io.opentelemetry.instrumentation.ktor.v2_0.server.KtorServerTracing
import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracingBuilder
import io.opentelemetry.instrumentation.ktor.server.AbstractKtorServerTracingBuilder
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
object KtorBuilderUtil {
lateinit var clientBuilderExtractor: (KtorClientTracingBuilder) -> DefaultHttpClientInstrumenterBuilder<HttpRequestData, HttpResponse>
lateinit var serverBuilderExtractor: (KtorServerTracing.Configuration) -> DefaultHttpServerInstrumenterBuilder<ApplicationRequest, ApplicationResponse>
lateinit var clientBuilderExtractor: (AbstractKtorClientTracingBuilder) -> DefaultHttpClientInstrumenterBuilder<HttpRequestData, HttpResponse>
lateinit var serverBuilderExtractor: (
AbstractKtorServerTracingBuilder
) -> DefaultHttpServerInstrumenterBuilder<ApplicationRequest, ApplicationResponse>
}

View File

@ -0,0 +1,88 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ktor.internal
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.util.*
import io.ktor.util.pipeline.*
import io.opentelemetry.context.Context
import io.opentelemetry.extension.kotlin.asContextElement
import io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount
import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracing
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
object KtorClientTracingUtil {
private val openTelemetryContextKey = AttributeKey<Context>("OpenTelemetry")
fun install(plugin: AbstractKtorClientTracing, scope: HttpClient) {
installSpanCreation(plugin, scope)
installSpanEnd(plugin, scope)
}
private fun installSpanCreation(plugin: AbstractKtorClientTracing, scope: HttpClient) {
val initializeRequestPhase = PipelinePhase("OpenTelemetryInitializeRequest")
scope.requestPipeline.insertPhaseAfter(HttpRequestPipeline.State, initializeRequestPhase)
scope.requestPipeline.intercept(initializeRequestPhase) {
val openTelemetryContext = HttpClientRequestResendCount.initialize(Context.current())
withContext(openTelemetryContext.asContextElement()) { proceed() }
}
val createSpanPhase = PipelinePhase("OpenTelemetryCreateSpan")
scope.sendPipeline.insertPhaseAfter(HttpSendPipeline.State, createSpanPhase)
scope.sendPipeline.intercept(createSpanPhase) {
val requestBuilder = context
val openTelemetryContext = plugin.createSpan(requestBuilder)
if (openTelemetryContext != null) {
try {
requestBuilder.attributes.put(openTelemetryContextKey, openTelemetryContext)
plugin.populateRequestHeaders(requestBuilder, openTelemetryContext)
withContext(openTelemetryContext.asContextElement()) { proceed() }
} catch (e: Throwable) {
plugin.endSpan(openTelemetryContext, requestBuilder, null, e)
throw e
}
} else {
proceed()
}
}
}
@OptIn(InternalCoroutinesApi::class)
private fun installSpanEnd(plugin: AbstractKtorClientTracing, scope: HttpClient) {
val endSpanPhase = PipelinePhase("OpenTelemetryEndSpan")
scope.receivePipeline.insertPhaseBefore(HttpReceivePipeline.State, endSpanPhase)
scope.receivePipeline.intercept(endSpanPhase) {
val openTelemetryContext = it.call.attributes.getOrNull(openTelemetryContextKey)
openTelemetryContext ?: return@intercept
scope.launch {
val job = it.call.coroutineContext.job
job.join()
val cause = if (!job.isCancelled) {
null
} else {
kotlin.runCatching { job.getCancellationException() }.getOrNull()
}
plugin.endSpan(openTelemetryContext, it.call, cause)
}
}
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ktor.internal
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.opentelemetry.context.Context
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
class KtorServerTracer(
private val instrumenter: Instrumenter<ApplicationRequest, ApplicationResponse>,
) {
fun start(call: ApplicationCall): Context? {
val parentContext = Context.current()
if (!instrumenter.shouldStart(parentContext, call.request)) {
return null
}
return instrumenter.start(parentContext, call.request)
}
fun end(context: Context, call: ApplicationCall, error: Throwable?) {
instrumenter.end(context, call.request, call.response, error)
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ktor.internal
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.util.*
import io.ktor.util.pipeline.*
import io.opentelemetry.context.Context
import io.opentelemetry.extension.kotlin.asContextElement
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor
import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil
import io.opentelemetry.instrumentation.ktor.server.AbstractKtorServerTracingBuilder
import io.opentelemetry.instrumentation.ktor.server.ApplicationRequestGetter
import kotlinx.coroutines.withContext
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
object KtorServerTracingUtil {
fun configureTracing(builder: AbstractKtorServerTracingBuilder, application: Application) {
val contextKey = AttributeKey<Context>("OpenTelemetry")
val errorKey = AttributeKey<Throwable>("OpenTelemetryException")
val instrumenter = instrumenter(builder)
val tracer = KtorServerTracer(instrumenter)
val startPhase = PipelinePhase("OpenTelemetry")
application.insertPhaseBefore(ApplicationCallPipeline.Monitoring, startPhase)
application.intercept(startPhase) {
val context = tracer.start(call)
if (context != null) {
call.attributes.put(contextKey, context)
withContext(context.asContextElement()) {
try {
proceed()
} catch (err: Throwable) {
// Stash error for reporting later since need ktor to finish setting up the response
call.attributes.put(errorKey, err)
throw err
}
}
} else {
proceed()
}
}
val postSendPhase = PipelinePhase("OpenTelemetryPostSend")
application.sendPipeline.insertPhaseAfter(ApplicationSendPipeline.After, postSendPhase)
application.sendPipeline.intercept(postSendPhase) {
val context = call.attributes.getOrNull(contextKey)
if (context != null) {
var error: Throwable? = call.attributes.getOrNull(errorKey)
try {
proceed()
} catch (t: Throwable) {
error = t
throw t
} finally {
tracer.end(context, call, error)
}
} else {
proceed()
}
}
}
private fun instrumenter(builder: AbstractKtorServerTracingBuilder): Instrumenter<ApplicationRequest, ApplicationResponse> {
return InstrumenterUtil.buildUpstreamInstrumenter(
builder.serverBuilder.instrumenterBuilder(),
ApplicationRequestGetter,
builder.spanKindExtractor(SpanKindExtractor.alwaysServer())
)
}
}

View File

@ -0,0 +1,196 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ktor.server
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.opentelemetry.api.OpenTelemetry
import io.opentelemetry.api.common.AttributesBuilder
import io.opentelemetry.api.trace.SpanKind
import io.opentelemetry.context.Context
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor
import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusBuilder
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor
import io.opentelemetry.instrumentation.ktor.internal.KtorBuilderUtil
abstract class AbstractKtorServerTracingBuilder(private val instrumentationName: String) {
companion object {
init {
KtorBuilderUtil.serverBuilderExtractor = { it.serverBuilder }
}
}
internal lateinit var serverBuilder: DefaultHttpServerInstrumenterBuilder<ApplicationRequest, ApplicationResponse>
internal var spanKindExtractor:
(SpanKindExtractor<ApplicationRequest>) -> SpanKindExtractor<ApplicationRequest> = { a -> a }
fun setOpenTelemetry(openTelemetry: OpenTelemetry) {
this.serverBuilder =
DefaultHttpServerInstrumenterBuilder.create(
instrumentationName,
openTelemetry,
KtorHttpServerAttributesGetter.INSTANCE
)
}
@Deprecated("Please use method `spanStatusExtractor`")
fun setStatusExtractor(
extractor: (SpanStatusExtractor<in ApplicationRequest, in ApplicationResponse>) -> SpanStatusExtractor<in ApplicationRequest, in ApplicationResponse>
) {
spanStatusExtractor { prevStatusExtractor ->
extractor(prevStatusExtractor).extract(spanStatusBuilder, request, response, error)
}
}
fun spanStatusExtractor(extract: SpanStatusData.(SpanStatusExtractor<in ApplicationRequest, in ApplicationResponse>) -> Unit) {
serverBuilder.setStatusExtractor { prevExtractor ->
SpanStatusExtractor { spanStatusBuilder: SpanStatusBuilder,
request: ApplicationRequest,
response: ApplicationResponse?,
throwable: Throwable? ->
extract(
SpanStatusData(spanStatusBuilder, request, response, throwable),
prevExtractor
)
}
}
}
data class SpanStatusData(
val spanStatusBuilder: SpanStatusBuilder,
val request: ApplicationRequest,
val response: ApplicationResponse?,
val error: Throwable?
)
@Deprecated("Please use method `spanKindExtractor`")
fun setSpanKindExtractor(extractor: (SpanKindExtractor<ApplicationRequest>) -> SpanKindExtractor<ApplicationRequest>) {
spanKindExtractor { prevSpanKindExtractor ->
extractor(prevSpanKindExtractor).extract(this)
}
}
fun spanKindExtractor(extract: ApplicationRequest.(SpanKindExtractor<ApplicationRequest>) -> SpanKind) {
spanKindExtractor = { prevExtractor ->
SpanKindExtractor<ApplicationRequest> { request: ApplicationRequest ->
extract(request, prevExtractor)
}
}
}
@Deprecated("Please use method `attributeExtractor`")
fun addAttributeExtractor(extractor: AttributesExtractor<in ApplicationRequest, in ApplicationResponse>) {
attributeExtractor {
onStart {
extractor.onStart(attributes, parentContext, request)
}
onEnd {
extractor.onEnd(attributes, parentContext, request, response, error)
}
}
}
fun attributeExtractor(extractorBuilder: ExtractorBuilder.() -> Unit = {}) {
val builder = ExtractorBuilder().apply(extractorBuilder).build()
serverBuilder.addAttributesExtractor(
object : AttributesExtractor<ApplicationRequest, ApplicationResponse> {
override fun onStart(attributes: AttributesBuilder, parentContext: Context, request: ApplicationRequest) {
builder.onStart(OnStartData(attributes, parentContext, request))
}
override fun onEnd(attributes: AttributesBuilder, context: Context, request: ApplicationRequest, response: ApplicationResponse?, error: Throwable?) {
builder.onEnd(OnEndData(attributes, context, request, response, error))
}
}
)
}
class ExtractorBuilder {
private var onStart: OnStartData.() -> Unit = {}
private var onEnd: OnEndData.() -> Unit = {}
fun onStart(block: OnStartData.() -> Unit) {
onStart = block
}
fun onEnd(block: OnEndData.() -> Unit) {
onEnd = block
}
internal fun build(): Extractor {
return Extractor(onStart, onEnd)
}
}
internal class Extractor(val onStart: OnStartData.() -> Unit, val onEnd: OnEndData.() -> Unit)
data class OnStartData(
val attributes: AttributesBuilder,
val parentContext: Context,
val request: ApplicationRequest
)
data class OnEndData(
val attributes: AttributesBuilder,
val parentContext: Context,
val request: ApplicationRequest,
val response: ApplicationResponse?,
val error: Throwable?
)
@Deprecated(
"Please use method `capturedRequestHeaders`",
ReplaceWith("capturedRequestHeaders(headers)")
)
fun setCapturedRequestHeaders(headers: List<String>) = capturedRequestHeaders(headers)
fun capturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())
fun capturedRequestHeaders(headers: Iterable<String>) {
serverBuilder.setCapturedRequestHeaders(headers.toList())
}
@Deprecated(
"Please use method `capturedResponseHeaders`",
ReplaceWith("capturedResponseHeaders(headers)")
)
fun setCapturedResponseHeaders(headers: List<String>) = capturedResponseHeaders(headers)
fun capturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())
fun capturedResponseHeaders(headers: Iterable<String>) {
serverBuilder.setCapturedResponseHeaders(headers.toList())
}
@Deprecated(
"Please use method `knownMethods`",
ReplaceWith("knownMethods(knownMethods)")
)
fun setKnownMethods(knownMethods: Set<String>) = knownMethods(knownMethods)
fun knownMethods(vararg methods: String) = knownMethods(methods.asIterable())
fun knownMethods(vararg methods: HttpMethod) = knownMethods(methods.asIterable())
@JvmName("knownMethodsJvm")
fun knownMethods(methods: Iterable<HttpMethod>) = knownMethods(methods.map { it.value })
fun knownMethods(methods: Iterable<String>) {
methods.toSet().apply {
serverBuilder.setKnownMethods(this)
}
}
/**
* {@link #setOpenTelemetry(OpenTelemetry)} sets the serverBuilder to a non-null value.
*/
fun isOpenTelemetryInitialized(): Boolean = this::serverBuilder.isInitialized
}

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ktor.v2_0.server
package io.opentelemetry.instrumentation.ktor.server
import io.ktor.server.request.*
import io.opentelemetry.context.propagation.TextMapGetter

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ktor.v2_0.server
package io.opentelemetry.instrumentation.ktor.server
import io.ktor.server.plugins.*
import io.ktor.server.request.*
@ -13,7 +13,7 @@ import io.opentelemetry.instrumentation.ktor.isIpAddress
internal enum class KtorHttpServerAttributesGetter :
HttpServerAttributesGetter<ApplicationRequest, ApplicationResponse> {
INSTANCE, ;
INSTANCE;
override fun getHttpRequestMethod(request: ApplicationRequest): String {
return request.httpMethod.value

View File

@ -7,10 +7,22 @@ plugins {
muzzle {
pass {
group.set("org.jetbrains.kotlinx")
module.set("ktor-server-core")
versions.set("[2.0.0,)")
group.set("io.ktor")
module.set("ktor-client-core")
versions.set("[2.0.0,3.0.0)")
assertInverse.set(true)
excludeInstrumentationName("ktor-server")
// missing dependencies
skip("1.1.0", "1.1.1", "1.1.5")
}
pass {
group.set("io.ktor")
module.set("ktor-server-core")
versions.set("[2.0.0,3.0.0)")
assertInverse.set(true)
excludeInstrumentationName("ktor-client")
// missing dependencies
skip("1.1.0", "1.1.1")
}
}
@ -25,6 +37,7 @@ dependencies {
compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent"))
testInstrumentation(project(":instrumentation:ktor:ktor-3.0:javaagent"))
testImplementation(project(":instrumentation:ktor:ktor-2.0:testing"))
testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")

View File

@ -13,9 +13,9 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import io.ktor.client.HttpClientConfig;
import io.ktor.client.engine.HttpClientEngineConfig;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.ktor.internal.KtorBuilderUtil;
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracing;
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder;
import io.opentelemetry.instrumentation.ktor.v2_0.internal.KtorBuilderUtil;
import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;

View File

@ -5,12 +5,14 @@
package io.opentelemetry.javaagent.instrumentation.ktor.v2_0;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static java.util.Collections.singletonList;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.List;
import net.bytebuddy.matcher.ElementMatcher;
@AutoService(InstrumentationModule.class)
public class KtorClientInstrumentationModule extends InstrumentationModule {
@ -24,6 +26,12 @@ public class KtorClientInstrumentationModule extends InstrumentationModule {
return className.startsWith("io.opentelemetry.extension.kotlin.");
}
@Override
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
// removed in ktor 3
return hasClassesNamed("io.ktor.client.engine.HttpClientJvmEngine");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new HttpClientInstrumentation());

View File

@ -11,8 +11,9 @@ import static net.bytebuddy.matcher.ElementMatchers.named;
import io.ktor.server.application.Application;
import io.ktor.server.application.ApplicationPluginKt;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.ktor.v2_0.internal.KtorBuilderUtil;
import io.opentelemetry.instrumentation.ktor.v2_0.server.KtorServerTracing;
import io.opentelemetry.instrumentation.ktor.internal.KtorBuilderUtil;
import io.opentelemetry.instrumentation.ktor.server.AbstractKtorServerTracingBuilder;
import io.opentelemetry.instrumentation.ktor.v2_0.server.KtorServerTracingBuilderKt;
import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
@ -39,19 +40,18 @@ public class ServerInstrumentation implements TypeInstrumentation {
@Advice.OnMethodExit
public static void onExit(@Advice.FieldValue("_applicationInstance") Application application) {
ApplicationPluginKt.install(application, KtorServerTracing.Feature, new SetupFunction());
ApplicationPluginKt.install(
application, KtorServerTracingBuilderKt.getKtorServerTracing(), new SetupFunction());
}
}
public static class SetupFunction
implements Function1<KtorServerTracing.Configuration, kotlin.Unit> {
implements Function1<AbstractKtorServerTracingBuilder, kotlin.Unit> {
@Override
public Unit invoke(KtorServerTracing.Configuration configuration) {
configuration.setOpenTelemetry(GlobalOpenTelemetry.get());
KtorBuilderUtil.serverBuilderExtractor
.invoke(configuration)
.configure(AgentCommonConfig.get());
public Unit invoke(AbstractKtorServerTracingBuilder builder) {
builder.setOpenTelemetry(GlobalOpenTelemetry.get());
KtorBuilderUtil.serverBuilderExtractor.invoke(builder).configure(AgentCommonConfig.get());
return kotlin.Unit.INSTANCE;
}
}

View File

@ -1,4 +1,4 @@
# Library Instrumentation for Ktor version 2.0 and higher
# Library Instrumentation for Ktor version 2.x
This package contains libraries to help instrument Ktor. Server and client instrumentations are supported.

View File

@ -13,7 +13,7 @@ dependencies {
library("io.ktor:ktor-client-core:$ktorVersion")
library("io.ktor:ktor-server-core:$ktorVersion")
implementation(project(":instrumentation:ktor:ktor-common:library"))
api(project(":instrumentation:ktor:ktor-2-common:library"))
implementation("io.opentelemetry:opentelemetry-extension-kotlin")
compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")

View File

@ -6,116 +6,28 @@
package io.opentelemetry.instrumentation.ktor.v2_0.client
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.util.*
import io.ktor.util.pipeline.*
import io.opentelemetry.context.Context
import io.opentelemetry.context.propagation.ContextPropagators
import io.opentelemetry.extension.kotlin.asContextElement
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
import io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracing
import io.opentelemetry.instrumentation.ktor.internal.KtorClientTracingUtil
class KtorClientTracing internal constructor(
private val instrumenter: Instrumenter<HttpRequestData, HttpResponse>,
private val propagators: ContextPropagators,
) {
private fun createSpan(requestBuilder: HttpRequestBuilder): Context? {
val parentContext = Context.current()
val requestData = requestBuilder.build()
return if (instrumenter.shouldStart(parentContext, requestData)) {
instrumenter.start(parentContext, requestData)
} else {
null
}
}
private fun populateRequestHeaders(requestBuilder: HttpRequestBuilder, context: Context) {
propagators.textMapPropagator.inject(context, requestBuilder, KtorHttpHeadersSetter)
}
private fun endSpan(context: Context, call: HttpClientCall, error: Throwable?) {
endSpan(context, HttpRequestBuilder().takeFrom(call.request), call.response, error)
}
private fun endSpan(context: Context, requestBuilder: HttpRequestBuilder, response: HttpResponse?, error: Throwable?) {
instrumenter.end(context, requestBuilder.build(), response, error)
}
instrumenter: Instrumenter<HttpRequestData, HttpResponse>,
propagators: ContextPropagators
) : AbstractKtorClientTracing(instrumenter, propagators) {
companion object : HttpClientPlugin<KtorClientTracingBuilder, KtorClientTracing> {
private val openTelemetryContextKey = AttributeKey<Context>("OpenTelemetry")
override val key = AttributeKey<KtorClientTracing>("OpenTelemetry")
override fun prepare(block: KtorClientTracingBuilder.() -> Unit) = KtorClientTracingBuilder().apply(block).build()
override fun install(plugin: KtorClientTracing, scope: HttpClient) {
installSpanCreation(plugin, scope)
installSpanEnd(plugin, scope)
}
private fun installSpanCreation(plugin: KtorClientTracing, scope: HttpClient) {
val initializeRequestPhase = PipelinePhase("OpenTelemetryInitializeRequest")
scope.requestPipeline.insertPhaseAfter(HttpRequestPipeline.State, initializeRequestPhase)
scope.requestPipeline.intercept(initializeRequestPhase) {
val openTelemetryContext = HttpClientRequestResendCount.initialize(Context.current())
withContext(openTelemetryContext.asContextElement()) { proceed() }
}
val createSpanPhase = PipelinePhase("OpenTelemetryCreateSpan")
scope.sendPipeline.insertPhaseAfter(HttpSendPipeline.State, createSpanPhase)
scope.sendPipeline.intercept(createSpanPhase) {
val requestBuilder = context
val openTelemetryContext = plugin.createSpan(requestBuilder)
if (openTelemetryContext != null) {
try {
requestBuilder.attributes.put(openTelemetryContextKey, openTelemetryContext)
plugin.populateRequestHeaders(requestBuilder, openTelemetryContext)
withContext(openTelemetryContext.asContextElement()) { proceed() }
} catch (e: Throwable) {
plugin.endSpan(openTelemetryContext, requestBuilder, null, e)
throw e
}
} else {
proceed()
}
}
}
@OptIn(InternalCoroutinesApi::class)
private fun installSpanEnd(plugin: KtorClientTracing, scope: HttpClient) {
val endSpanPhase = PipelinePhase("OpenTelemetryEndSpan")
scope.receivePipeline.insertPhaseBefore(HttpReceivePipeline.State, endSpanPhase)
scope.receivePipeline.intercept(endSpanPhase) {
val openTelemetryContext = it.call.attributes.getOrNull(openTelemetryContextKey)
openTelemetryContext ?: return@intercept
scope.launch {
val job = it.call.coroutineContext.job
job.join()
val cause = if (!job.isCancelled) {
null
} else {
kotlin.runCatching { job.getCancellationException() }.getOrNull()
}
plugin.endSpan(openTelemetryContext, it.call, cause)
}
}
KtorClientTracingUtil.install(plugin, scope)
}
}
}

View File

@ -5,168 +5,13 @@
package io.opentelemetry.instrumentation.ktor.v2_0.client
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.opentelemetry.api.OpenTelemetry
import io.opentelemetry.api.common.AttributesBuilder
import io.opentelemetry.context.Context
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor
import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracingBuilder
import io.opentelemetry.instrumentation.ktor.v2_0.InstrumentationProperties.INSTRUMENTATION_NAME
import io.opentelemetry.instrumentation.ktor.v2_0.internal.KtorBuilderUtil
class KtorClientTracingBuilder {
companion object {
init {
KtorBuilderUtil.clientBuilderExtractor = { it.clientBuilder }
}
}
private lateinit var openTelemetry: OpenTelemetry
private lateinit var clientBuilder: DefaultHttpClientInstrumenterBuilder<HttpRequestData, HttpResponse>
fun setOpenTelemetry(openTelemetry: OpenTelemetry) {
this.openTelemetry = openTelemetry
this.clientBuilder = DefaultHttpClientInstrumenterBuilder.create(
INSTRUMENTATION_NAME,
openTelemetry,
KtorHttpClientAttributesGetter
)
}
@Deprecated(
"Please use method `capturedRequestHeaders`",
ReplaceWith("capturedRequestHeaders(headers.asIterable())")
)
fun setCapturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())
@Deprecated(
"Please use method `capturedRequestHeaders`",
ReplaceWith("capturedRequestHeaders(headers)")
)
fun setCapturedRequestHeaders(headers: List<String>) = capturedRequestHeaders(headers)
fun capturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())
fun capturedRequestHeaders(headers: Iterable<String>) {
clientBuilder.setCapturedRequestHeaders(headers.toList())
}
@Deprecated(
"Please use method `capturedResponseHeaders`",
ReplaceWith("capturedResponseHeaders(headers.asIterable())")
)
fun setCapturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())
@Deprecated(
"Please use method `capturedResponseHeaders`",
ReplaceWith("capturedResponseHeaders(headers)")
)
fun setCapturedResponseHeaders(headers: List<String>) = capturedResponseHeaders(headers)
fun capturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())
fun capturedResponseHeaders(headers: Iterable<String>) {
clientBuilder.setCapturedResponseHeaders(headers.toList())
}
@Deprecated(
"Please use method `knownMethods`",
ReplaceWith("knownMethods(knownMethods)")
)
fun setKnownMethods(knownMethods: Set<String>) = knownMethods(knownMethods)
fun knownMethods(vararg methods: String) = knownMethods(methods.asIterable())
fun knownMethods(vararg methods: HttpMethod) = knownMethods(methods.asIterable())
@JvmName("knownMethodsJvm")
fun knownMethods(methods: Iterable<HttpMethod>) = knownMethods(methods.map { it.value })
fun knownMethods(methods: Iterable<String>) {
clientBuilder.setKnownMethods(methods.toSet())
}
@Deprecated("Please use method `attributeExtractor`")
fun addAttributesExtractors(vararg extractors: AttributesExtractor<in HttpRequestData, in HttpResponse>) = addAttributesExtractors(extractors.asList())
@Deprecated("Please use method `attributeExtractor`")
fun addAttributesExtractors(extractors: Iterable<AttributesExtractor<in HttpRequestData, in HttpResponse>>) {
extractors.forEach {
attributeExtractor {
onStart { it.onStart(attributes, parentContext, request) }
onEnd { it.onEnd(attributes, parentContext, request, response, error) }
}
}
}
fun attributeExtractor(extractorBuilder: ExtractorBuilder.() -> Unit = {}) {
val builder = ExtractorBuilder().apply(extractorBuilder).build()
this.clientBuilder.addAttributeExtractor(
object : AttributesExtractor<HttpRequestData, HttpResponse> {
override fun onStart(attributes: AttributesBuilder, parentContext: Context, request: HttpRequestData) {
builder.onStart(OnStartData(attributes, parentContext, request))
}
override fun onEnd(attributes: AttributesBuilder, context: Context, request: HttpRequestData, response: HttpResponse?, error: Throwable?) {
builder.onEnd(OnEndData(attributes, context, request, response, error))
}
}
)
}
class ExtractorBuilder {
private var onStart: OnStartData.() -> Unit = {}
private var onEnd: OnEndData.() -> Unit = {}
fun onStart(block: OnStartData.() -> Unit) {
onStart = block
}
fun onEnd(block: OnEndData.() -> Unit) {
onEnd = block
}
internal fun build(): Extractor {
return Extractor(onStart, onEnd)
}
}
internal class Extractor(val onStart: OnStartData.() -> Unit, val onEnd: OnEndData.() -> Unit)
data class OnStartData(
val attributes: AttributesBuilder,
val parentContext: Context,
val request: HttpRequestData
)
data class OnEndData(
val attributes: AttributesBuilder,
val parentContext: Context,
val request: HttpRequestData,
val response: HttpResponse?,
val error: Throwable?
)
/**
* Configures the instrumentation to emit experimental HTTP client metrics.
*
* @param emitExperimentalHttpClientMetrics `true` if the experimental HTTP client metrics are to be emitted.
*/
@Deprecated("Please use method `emitExperimentalHttpClientMetrics`")
fun setEmitExperimentalHttpClientMetrics(emitExperimentalHttpClientMetrics: Boolean) {
if (emitExperimentalHttpClientMetrics) {
emitExperimentalHttpClientMetrics()
}
}
fun emitExperimentalHttpClientMetrics() {
clientBuilder.setEmitExperimentalHttpClientMetrics(true)
}
class KtorClientTracingBuilder : AbstractKtorClientTracingBuilder(INSTRUMENTATION_NAME) {
internal fun build(): KtorClientTracing = KtorClientTracing(
instrumenter = clientBuilder.build(),
propagators = openTelemetry.propagators,
propagators = getOpenTelemetry().propagators,
)
}

View File

@ -1,292 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ktor.v2_0.server
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.*
import io.ktor.util.pipeline.*
import io.opentelemetry.api.OpenTelemetry
import io.opentelemetry.api.common.AttributesBuilder
import io.opentelemetry.api.trace.SpanKind
import io.opentelemetry.context.Context
import io.opentelemetry.extension.kotlin.asContextElement
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusBuilder
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor
import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource
import io.opentelemetry.instrumentation.ktor.v2_0.InstrumentationProperties.INSTRUMENTATION_NAME
import io.opentelemetry.instrumentation.ktor.v2_0.internal.KtorBuilderUtil
import kotlinx.coroutines.withContext
class KtorServerTracing private constructor(
private val instrumenter: Instrumenter<ApplicationRequest, ApplicationResponse>,
) {
class Configuration {
companion object {
init {
KtorBuilderUtil.serverBuilderExtractor = { it.serverBuilder }
}
}
internal lateinit var serverBuilder: DefaultHttpServerInstrumenterBuilder<ApplicationRequest, ApplicationResponse>
internal var spanKindExtractor:
(SpanKindExtractor<ApplicationRequest>) -> SpanKindExtractor<ApplicationRequest> = { a -> a }
fun setOpenTelemetry(openTelemetry: OpenTelemetry) {
this.serverBuilder =
DefaultHttpServerInstrumenterBuilder.create(
INSTRUMENTATION_NAME,
openTelemetry,
KtorHttpServerAttributesGetter.INSTANCE
)
}
@Deprecated("Please use method `spanStatusExtractor`")
fun setStatusExtractor(
extractor: (SpanStatusExtractor<in ApplicationRequest, in ApplicationResponse>) -> SpanStatusExtractor<in ApplicationRequest, in ApplicationResponse>
) {
spanStatusExtractor { prevStatusExtractor ->
extractor(prevStatusExtractor).extract(spanStatusBuilder, request, response, error)
}
}
fun spanStatusExtractor(extract: SpanStatusData.(SpanStatusExtractor<in ApplicationRequest, in ApplicationResponse>) -> Unit) {
serverBuilder.setStatusExtractor { prevExtractor ->
SpanStatusExtractor { spanStatusBuilder: SpanStatusBuilder,
request: ApplicationRequest,
response: ApplicationResponse?,
throwable: Throwable? ->
extract(
SpanStatusData(spanStatusBuilder, request, response, throwable),
prevExtractor
)
}
}
}
data class SpanStatusData(
val spanStatusBuilder: SpanStatusBuilder,
val request: ApplicationRequest,
val response: ApplicationResponse?,
val error: Throwable?
)
@Deprecated("Please use method `spanKindExtractor`")
fun setSpanKindExtractor(extractor: (SpanKindExtractor<ApplicationRequest>) -> SpanKindExtractor<ApplicationRequest>) {
spanKindExtractor { prevSpanKindExtractor ->
extractor(prevSpanKindExtractor).extract(this)
}
}
fun spanKindExtractor(extract: ApplicationRequest.(SpanKindExtractor<ApplicationRequest>) -> SpanKind) {
spanKindExtractor = { prevExtractor ->
SpanKindExtractor<ApplicationRequest> { request: ApplicationRequest ->
extract(request, prevExtractor)
}
}
}
@Deprecated("Please use method `attributeExtractor`")
fun addAttributeExtractor(extractor: AttributesExtractor<in ApplicationRequest, in ApplicationResponse>) {
attributeExtractor {
onStart {
extractor.onStart(attributes, parentContext, request)
}
onEnd {
extractor.onEnd(attributes, parentContext, request, response, error)
}
}
}
fun attributeExtractor(extractorBuilder: ExtractorBuilder.() -> Unit = {}) {
val builder = ExtractorBuilder().apply(extractorBuilder).build()
serverBuilder.addAttributesExtractor(
object : AttributesExtractor<ApplicationRequest, ApplicationResponse> {
override fun onStart(attributes: AttributesBuilder, parentContext: Context, request: ApplicationRequest) {
builder.onStart(OnStartData(attributes, parentContext, request))
}
override fun onEnd(attributes: AttributesBuilder, context: Context, request: ApplicationRequest, response: ApplicationResponse?, error: Throwable?) {
builder.onEnd(OnEndData(attributes, context, request, response, error))
}
}
)
}
class ExtractorBuilder {
private var onStart: OnStartData.() -> Unit = {}
private var onEnd: OnEndData.() -> Unit = {}
fun onStart(block: OnStartData.() -> Unit) {
onStart = block
}
fun onEnd(block: OnEndData.() -> Unit) {
onEnd = block
}
internal fun build(): Extractor {
return Extractor(onStart, onEnd)
}
}
internal class Extractor(val onStart: OnStartData.() -> Unit, val onEnd: OnEndData.() -> Unit)
data class OnStartData(
val attributes: AttributesBuilder,
val parentContext: Context,
val request: ApplicationRequest
)
data class OnEndData(
val attributes: AttributesBuilder,
val parentContext: Context,
val request: ApplicationRequest,
val response: ApplicationResponse?,
val error: Throwable?
)
@Deprecated(
"Please use method `capturedRequestHeaders`",
ReplaceWith("capturedRequestHeaders(headers)")
)
fun setCapturedRequestHeaders(headers: List<String>) = capturedRequestHeaders(headers)
fun capturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())
fun capturedRequestHeaders(headers: Iterable<String>) {
serverBuilder.setCapturedRequestHeaders(headers.toList())
}
@Deprecated(
"Please use method `capturedResponseHeaders`",
ReplaceWith("capturedResponseHeaders(headers)")
)
fun setCapturedResponseHeaders(headers: List<String>) = capturedResponseHeaders(headers)
fun capturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())
fun capturedResponseHeaders(headers: Iterable<String>) {
serverBuilder.setCapturedResponseHeaders(headers.toList())
}
@Deprecated(
"Please use method `knownMethods`",
ReplaceWith("knownMethods(knownMethods)")
)
fun setKnownMethods(knownMethods: Set<String>) = knownMethods(knownMethods)
fun knownMethods(vararg methods: String) = knownMethods(methods.asIterable())
fun knownMethods(vararg methods: HttpMethod) = knownMethods(methods.asIterable())
@JvmName("knownMethodsJvm")
fun knownMethods(methods: Iterable<HttpMethod>) = knownMethods(methods.map { it.value })
fun knownMethods(methods: Iterable<String>) {
methods.toSet().apply {
serverBuilder.setKnownMethods(this)
}
}
/**
* {@link #setOpenTelemetry(OpenTelemetry)} sets the serverBuilder to a non-null value.
*/
internal fun isOpenTelemetryInitialized(): Boolean = this::serverBuilder.isInitialized
}
private fun start(call: ApplicationCall): Context? {
val parentContext = Context.current()
if (!instrumenter.shouldStart(parentContext, call.request)) {
return null
}
return instrumenter.start(parentContext, call.request)
}
private fun end(context: Context, call: ApplicationCall, error: Throwable?) {
instrumenter.end(context, call.request, call.response, error)
}
companion object Feature : BaseApplicationPlugin<Application, Configuration, KtorServerTracing> {
private val contextKey = AttributeKey<Context>("OpenTelemetry")
private val errorKey = AttributeKey<Throwable>("OpenTelemetryException")
override val key: AttributeKey<KtorServerTracing> = AttributeKey("OpenTelemetry")
override fun install(pipeline: Application, configure: Configuration.() -> Unit): KtorServerTracing {
val configuration = Configuration().apply(configure)
require(configuration.isOpenTelemetryInitialized()) { "OpenTelemetry must be set" }
val instrumenter = InstrumenterUtil.buildUpstreamInstrumenter(
configuration.serverBuilder.instrumenterBuilder(),
ApplicationRequestGetter,
configuration.spanKindExtractor(SpanKindExtractor.alwaysServer())
)
val feature = KtorServerTracing(instrumenter)
val startPhase = PipelinePhase("OpenTelemetry")
pipeline.insertPhaseBefore(ApplicationCallPipeline.Monitoring, startPhase)
pipeline.intercept(startPhase) {
val context = feature.start(call)
if (context != null) {
call.attributes.put(contextKey, context)
withContext(context.asContextElement()) {
try {
proceed()
} catch (err: Throwable) {
// Stash error for reporting later since need ktor to finish setting up the response
call.attributes.put(errorKey, err)
throw err
}
}
} else {
proceed()
}
}
val postSendPhase = PipelinePhase("OpenTelemetryPostSend")
pipeline.sendPipeline.insertPhaseAfter(ApplicationSendPipeline.After, postSendPhase)
pipeline.sendPipeline.intercept(postSendPhase) {
val context = call.attributes.getOrNull(contextKey)
if (context != null) {
var error: Throwable? = call.attributes.getOrNull(errorKey)
try {
proceed()
} catch (t: Throwable) {
error = t
throw t
} finally {
feature.end(context, call, error)
}
} else {
proceed()
}
}
pipeline.environment.monitor.subscribe(Routing.RoutingCallStarted) { call ->
HttpServerRoute.update(Context.current(), HttpServerRouteSource.SERVER, { _, arg -> arg.route.parent.toString() }, call)
}
return feature
}
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ktor.v2_0.server
import io.ktor.server.application.*
import io.ktor.server.routing.*
import io.opentelemetry.context.Context
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource
import io.opentelemetry.instrumentation.ktor.internal.KtorServerTracingUtil
import io.opentelemetry.instrumentation.ktor.server.AbstractKtorServerTracingBuilder
import io.opentelemetry.instrumentation.ktor.v2_0.InstrumentationProperties.INSTRUMENTATION_NAME
class KtorServerTracingBuilder internal constructor(
instrumentationName: String
) : AbstractKtorServerTracingBuilder(instrumentationName)
val KtorServerTracing = createRouteScopedPlugin("OpenTelemetry", { KtorServerTracingBuilder(INSTRUMENTATION_NAME) }) {
require(pluginConfig.isOpenTelemetryInitialized()) { "OpenTelemetry must be set" }
KtorServerTracingUtil.configureTracing(pluginConfig, application)
application.environment.monitor.subscribe(Routing.RoutingCallStarted) { call ->
HttpServerRoute.update(Context.current(), HttpServerRouteSource.SERVER, { _, arg -> arg.route.parent.toString() }, call)
}
}

View File

@ -18,6 +18,7 @@ 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 org.junit.jupiter.api.AfterAll
import java.net.URI
abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest<HttpRequestBuilder>() {
@ -27,6 +28,19 @@ abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest<HttpRequestBu
installTracing()
}
private val singleConnectionClient = HttpClient(CIO) {
engine {
maxConnectionsCount = 1
}
installTracing()
}
@AfterAll
fun tearDown() {
client.close()
singleConnectionClient.close()
}
abstract fun HttpClientConfig<*>.installTracing()
@ -67,7 +81,7 @@ abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest<HttpRequestBu
setHttpAttributes { DEFAULT_HTTP_ATTRIBUTES - setOf(NetworkAttributes.NETWORK_PROTOCOL_VERSION) }
setSingleConnectionFactory { host, port ->
KtorHttpClientSingleConnection(host, port) { installTracing() }
KtorHttpClientSingleConnection(singleConnectionClient, host, port)
}
}
}

View File

@ -12,23 +12,11 @@ import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection
import kotlinx.coroutines.runBlocking
class KtorHttpClientSingleConnection(
private val client: HttpClient,
private val host: String,
private val port: Int,
private val installTracing: HttpClientConfig<*>.() -> Unit,
private val port: Int
) : 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",

View File

@ -0,0 +1,56 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("org.jetbrains.kotlin.jvm")
id("otel.javaagent-instrumentation")
}
muzzle {
pass {
group.set("io.ktor")
module.set("ktor-client-core")
versions.set("[3.0.0,)")
assertInverse.set(true)
excludeInstrumentationName("ktor-server")
// missing dependencies
skip("1.1.0", "1.1.1", "1.1.5")
}
pass {
group.set("io.ktor")
module.set("ktor-server-core")
versions.set("[3.0.0,)")
assertInverse.set(true)
excludeInstrumentationName("ktor-client")
// missing dependencies
skip("1.1.0", "1.1.1")
}
}
val ktorVersion = "3.0.0"
dependencies {
library("io.ktor:ktor-client-core:$ktorVersion")
library("io.ktor:ktor-server-core:$ktorVersion")
implementation(project(":instrumentation:ktor:ktor-3.0:library"))
compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent"))
testInstrumentation(project(":instrumentation:ktor:ktor-2.0:javaagent"))
testImplementation(project(":instrumentation:ktor:ktor-3.0:testing"))
testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("io.opentelemetry:opentelemetry-extension-kotlin")
testLibrary("io.ktor:ktor-server-netty:$ktorVersion")
testLibrary("io.ktor:ktor-client-cio:$ktorVersion")
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_1_8)
// generate metadata for Java 1.8 reflection on method parameters, used in @WithSpan tests
javaParameters = true
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.ktor.v3_0;
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import io.ktor.client.HttpClientConfig;
import io.ktor.client.engine.HttpClientEngineConfig;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.ktor.internal.KtorBuilderUtil;
import io.opentelemetry.instrumentation.ktor.v3_0.client.KtorClientTracing;
import io.opentelemetry.instrumentation.ktor.v3_0.client.KtorClientTracingBuilder;
import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
public class HttpClientInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("io.ktor.client.HttpClient");
}
@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
isConstructor()
.and(takesArguments(2))
.and(takesArgument(1, named("io.ktor.client.HttpClientConfig"))),
this.getClass().getName() + "$ConstructorAdvice");
}
@SuppressWarnings("unused")
public static class ConstructorAdvice {
@Advice.OnMethodEnter
public static void onEnter(
@Advice.Argument(1) HttpClientConfig<HttpClientEngineConfig> httpClientConfig) {
httpClientConfig.install(KtorClientTracing.Companion, new SetupFunction());
}
}
public static class SetupFunction implements Function1<KtorClientTracingBuilder, Unit> {
@Override
public Unit invoke(KtorClientTracingBuilder builder) {
builder.setOpenTelemetry(GlobalOpenTelemetry.get());
KtorBuilderUtil.clientBuilderExtractor.invoke(builder).configure(AgentCommonConfig.get());
return Unit.INSTANCE;
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.ktor.v3_0;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static java.util.Collections.singletonList;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.List;
import net.bytebuddy.matcher.ElementMatcher;
@AutoService(InstrumentationModule.class)
public class KtorClientInstrumentationModule extends InstrumentationModule {
public KtorClientInstrumentationModule() {
super("ktor", "ktor-client", "ktor-3.0", "ktor-client-3.0");
}
@Override
public boolean isHelperClass(String className) {
return className.startsWith("io.opentelemetry.extension.kotlin.");
}
@Override
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
// added in ktor 3
return hasClassesNamed("io.ktor.client.content.ProgressListener");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new HttpClientInstrumentation());
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.ktor.v3_0;
import static java.util.Collections.singletonList;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.List;
@AutoService(InstrumentationModule.class)
public class KtorServerInstrumentationModule extends InstrumentationModule {
public KtorServerInstrumentationModule() {
super("ktor", "ktor-server", "ktor-3.0", "ktor-server-3.0");
}
@Override
public boolean isHelperClass(String className) {
return className.startsWith("io.opentelemetry.extension.kotlin.");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new ServerInstrumentation());
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.ktor.v3_0;
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
import static net.bytebuddy.matcher.ElementMatchers.named;
import io.ktor.server.application.Application;
import io.ktor.server.application.ApplicationPluginKt;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.ktor.internal.KtorBuilderUtil;
import io.opentelemetry.instrumentation.ktor.server.AbstractKtorServerTracingBuilder;
import io.opentelemetry.instrumentation.ktor.v3_0.server.KtorServerTracingBuilderKt;
import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
public class ServerInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("io.ktor.server.engine.EmbeddedServer");
}
@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
isConstructor(), this.getClass().getName() + "$ConstructorAdvice");
}
@SuppressWarnings("unused")
public static class ConstructorAdvice {
@Advice.OnMethodExit
public static void onExit(@Advice.FieldValue("_applicationInstance") Application application) {
ApplicationPluginKt.install(
application, KtorServerTracingBuilderKt.getKtorServerTracing(), new SetupFunction());
}
}
public static class SetupFunction implements Function1<AbstractKtorServerTracingBuilder, Unit> {
@Override
public Unit invoke(AbstractKtorServerTracingBuilder builder) {
builder.setOpenTelemetry(GlobalOpenTelemetry.get());
KtorBuilderUtil.serverBuilderExtractor.invoke(builder).configure(AgentCommonConfig.get());
return Unit.INSTANCE;
}
}
}

View File

@ -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 KtorHttpClientTest : AbstractKtorHttpClientTest() {
companion object {
@JvmStatic
@RegisterExtension
private val TESTING = HttpClientInstrumentationExtension.forAgent()
}
override fun HttpClientConfig<*>.installTracing() {
}
}

View File

@ -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 KtorHttpServerTest : 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)
}
}

View File

@ -0,0 +1,60 @@
# Library Instrumentation for Ktor version 3.0 and higher
This package contains libraries to help instrument Ktor. Server and client instrumentations are supported.
## Quickstart
### Add these dependencies to your project
Replace `OPENTELEMETRY_VERSION` with the [latest
release](https://search.maven.org/search?q=g:io.opentelemetry.instrumentation%20AND%20a:opentelemetry-ktor-3.0).
For Maven, add to your `pom.xml` dependencies:
```xml
<dependencies>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-ktor-3.0</artifactId>
<version>OPENTELEMETRY_VERSION</version>
</dependency>
</dependencies>
```
For Gradle, add to your dependencies:
```groovy
implementation("io.opentelemetry.instrumentation:opentelemetry-ktor-3.0:OPENTELEMETRY_VERSION")
```
## Usage
## Initializing server instrumentation
Initialize instrumentation by installing the `KtorServerTracing` feature. You must set the `OpenTelemetry` to use with
the feature.
```kotlin
val openTelemetry: OpenTelemetry = ...
embeddedServer(Netty, 8080) {
install(KtorServerTracing) {
setOpenTelemetry(openTelemetry)
}
}
```
## Initializing client instrumentation
Initialize instrumentation by installing the `KtorClientTracing` feature. You must set the `OpenTelemetry` to use with
the feature.
```kotlin
val openTelemetry: OpenTelemetry = ...
val client = HttpClient {
install(KtorClientTracing) {
setOpenTelemetry(openTelemetry)
}
}
```

View File

@ -0,0 +1,34 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
plugins {
id("otel.library-instrumentation")
id("org.jetbrains.kotlin.jvm")
}
val ktorVersion = "3.0.0"
dependencies {
library("io.ktor:ktor-client-core:$ktorVersion")
library("io.ktor:ktor-server-core:$ktorVersion")
api(project(":instrumentation:ktor:ktor-2-common:library"))
implementation("io.opentelemetry:opentelemetry-extension-kotlin")
compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation(project(":instrumentation:ktor:ktor-3.0:testing"))
testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testLibrary("io.ktor:ktor-server-netty:$ktorVersion")
testLibrary("io.ktor:ktor-client-cio:$ktorVersion")
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_1_8)
@Suppress("deprecation")
languageVersion.set(KotlinVersion.KOTLIN_1_6)
}
}

View File

@ -0,0 +1,14 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ktor.v3_0
/**
* Common properties for both client and server instrumentations
*/
internal object InstrumentationProperties {
internal const val INSTRUMENTATION_NAME = "io.opentelemetry.ktor-3.0"
}

View File

@ -0,0 +1,33 @@
/*
* 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.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.util.*
import io.opentelemetry.context.propagation.ContextPropagators
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracing
import io.opentelemetry.instrumentation.ktor.internal.KtorClientTracingUtil
class KtorClientTracing internal constructor(
instrumenter: Instrumenter<HttpRequestData, HttpResponse>,
propagators: ContextPropagators
) : AbstractKtorClientTracing(instrumenter, propagators) {
companion object : HttpClientPlugin<KtorClientTracingBuilder, KtorClientTracing> {
override val key = AttributeKey<KtorClientTracing>("OpenTelemetry")
override fun prepare(block: KtorClientTracingBuilder.() -> Unit) = KtorClientTracingBuilder().apply(block).build()
override fun install(plugin: KtorClientTracing, scope: HttpClient) {
KtorClientTracingUtil.install(plugin, scope)
}
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.ktor.v3_0.client
import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracingBuilder
import io.opentelemetry.instrumentation.ktor.v3_0.InstrumentationProperties.INSTRUMENTATION_NAME
class KtorClientTracingBuilder : AbstractKtorClientTracingBuilder(INSTRUMENTATION_NAME) {
internal fun build(): KtorClientTracing = KtorClientTracing(
instrumenter = clientBuilder.build(),
propagators = getOpenTelemetry().propagators,
)
}

View File

@ -0,0 +1,29 @@
/*
* 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.ktor.server.routing.*
import io.opentelemetry.context.Context
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource
import io.opentelemetry.instrumentation.ktor.internal.KtorServerTracingUtil
import io.opentelemetry.instrumentation.ktor.server.AbstractKtorServerTracingBuilder
import io.opentelemetry.instrumentation.ktor.v3_0.InstrumentationProperties.INSTRUMENTATION_NAME
class KtorServerTracingBuilder internal constructor(
instrumentationName: String
) : AbstractKtorServerTracingBuilder(instrumentationName)
val KtorServerTracing = createRouteScopedPlugin("OpenTelemetry", { KtorServerTracingBuilder(INSTRUMENTATION_NAME) }) {
require(pluginConfig.isOpenTelemetryInitialized()) { "OpenTelemetry must be set" }
KtorServerTracingUtil.configureTracing(pluginConfig, application)
application.monitor.subscribe(RoutingRoot.RoutingCallStarted) { call ->
HttpServerRoute.update(Context.current(), HttpServerRouteSource.SERVER, { _, arg -> arg.route.parent.toString() }, call)
}
}

View File

@ -0,0 +1,27 @@
/*
* 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 KtorHttpClientTest : AbstractKtorHttpClientTest() {
companion object {
@JvmStatic
@RegisterExtension
private val TESTING = HttpClientInstrumentationExtension.forLibrary()
}
override fun HttpClientConfig<*>.installTracing() {
install(KtorClientTracing) {
setOpenTelemetry(TESTING.openTelemetry)
capturedRequestHeaders(TEST_REQUEST_HEADER)
capturedResponseHeaders(TEST_RESPONSE_HEADER)
}
}
}

View File

@ -0,0 +1,34 @@
/*
* 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 org.junit.jupiter.api.extension.RegisterExtension
class KtorHttpServerTest : AbstractKtorHttpServerTest() {
companion object {
@JvmStatic
@RegisterExtension
val TESTING: InstrumentationExtension = HttpServerInstrumentationExtension.forLibrary()
}
override fun getTesting(): InstrumentationExtension {
return TESTING
}
override fun installOpenTelemetry(application: Application) {
application.apply {
install(KtorServerTracing) {
setOpenTelemetry(TESTING.openTelemetry)
capturedRequestHeaders(TEST_REQUEST_HEADER)
capturedResponseHeaders(TEST_RESPONSE_HEADER)
}
}
}
}

View File

@ -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)
}
}

View File

@ -0,0 +1,88 @@
/*
* 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 org.junit.jupiter.api.AfterAll
import java.net.URI
abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest<HttpRequestBuilder>() {
private val client = HttpClient(CIO) {
install(HttpRedirect)
installTracing()
}
private val singleConnectionClient = HttpClient(CIO) {
engine {
maxConnectionsCount = 1
}
installTracing()
}
@AfterAll
fun tearDown() {
client.close()
singleConnectionClient.close()
}
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()
markAsLowLevelInstrumentation()
setMaxRedirects(20)
spanEndsAfterBody()
setHttpAttributes { DEFAULT_HTTP_ATTRIBUTES - setOf(NetworkAttributes.NETWORK_PROTOCOL_VERSION) }
setSingleConnectionFactory { host, port ->
KtorHttpClientSingleConnection(singleConnectionClient, host, port)
}
}
}
}

View File

@ -0,0 +1,32 @@
/*
* 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 client: HttpClient,
private val host: String,
private val port: Int
) : SingleConnection {
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
}
}

View File

@ -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 IllegalStateException(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)
}
}

View File

@ -402,6 +402,10 @@ 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-2-common:library")
include(":instrumentation:ktor:ktor-3.0:javaagent")
include(":instrumentation:ktor:ktor-3.0:library")
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")