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] | | [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] | | [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 | | [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] | | [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] | | [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 | | [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. 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 * 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.request.*
import io.ktor.client.statement.* import io.ktor.client.statement.*

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0 * 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.ktor.client.request.HttpRequestBuilder
import io.opentelemetry.context.propagation.TextMapSetter import io.opentelemetry.context.propagation.TextMapSetter

View File

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

View File

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

View File

@ -7,10 +7,22 @@ plugins {
muzzle { muzzle {
pass { pass {
group.set("org.jetbrains.kotlinx") group.set("io.ktor")
module.set("ktor-server-core") module.set("ktor-client-core")
versions.set("[2.0.0,)") versions.set("[2.0.0,3.0.0)")
assertInverse.set(true) 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") compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) 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(project(":instrumentation:ktor:ktor-2.0:testing"))
testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") 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.HttpClientConfig;
import io.ktor.client.engine.HttpClientEngineConfig; import io.ktor.client.engine.HttpClientEngineConfig;
import io.opentelemetry.api.GlobalOpenTelemetry; 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.KtorClientTracing;
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder; 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.bootstrap.internal.AgentCommonConfig;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;

View File

@ -5,12 +5,14 @@
package io.opentelemetry.javaagent.instrumentation.ktor.v2_0; package io.opentelemetry.javaagent.instrumentation.ktor.v2_0;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import com.google.auto.service.AutoService; import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.List; import java.util.List;
import net.bytebuddy.matcher.ElementMatcher;
@AutoService(InstrumentationModule.class) @AutoService(InstrumentationModule.class)
public class KtorClientInstrumentationModule extends InstrumentationModule { public class KtorClientInstrumentationModule extends InstrumentationModule {
@ -24,6 +26,12 @@ public class KtorClientInstrumentationModule extends InstrumentationModule {
return className.startsWith("io.opentelemetry.extension.kotlin."); 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 @Override
public List<TypeInstrumentation> typeInstrumentations() { public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new HttpClientInstrumentation()); 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.Application;
import io.ktor.server.application.ApplicationPluginKt; import io.ktor.server.application.ApplicationPluginKt;
import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.ktor.v2_0.internal.KtorBuilderUtil; import io.opentelemetry.instrumentation.ktor.internal.KtorBuilderUtil;
import io.opentelemetry.instrumentation.ktor.v2_0.server.KtorServerTracing; 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.bootstrap.internal.AgentCommonConfig;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
@ -39,19 +40,18 @@ public class ServerInstrumentation implements TypeInstrumentation {
@Advice.OnMethodExit @Advice.OnMethodExit
public static void onExit(@Advice.FieldValue("_applicationInstance") Application application) { 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 public static class SetupFunction
implements Function1<KtorServerTracing.Configuration, kotlin.Unit> { implements Function1<AbstractKtorServerTracingBuilder, kotlin.Unit> {
@Override @Override
public Unit invoke(KtorServerTracing.Configuration configuration) { public Unit invoke(AbstractKtorServerTracingBuilder builder) {
configuration.setOpenTelemetry(GlobalOpenTelemetry.get()); builder.setOpenTelemetry(GlobalOpenTelemetry.get());
KtorBuilderUtil.serverBuilderExtractor KtorBuilderUtil.serverBuilderExtractor.invoke(builder).configure(AgentCommonConfig.get());
.invoke(configuration)
.configure(AgentCommonConfig.get());
return kotlin.Unit.INSTANCE; 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. 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-client-core:$ktorVersion")
library("io.ktor:ktor-server-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") implementation("io.opentelemetry:opentelemetry-extension-kotlin")
compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8") compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")

View File

@ -6,116 +6,28 @@
package io.opentelemetry.instrumentation.ktor.v2_0.client package io.opentelemetry.instrumentation.ktor.v2_0.client
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.* import io.ktor.client.plugins.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
import io.ktor.util.* import io.ktor.util.*
import io.ktor.util.pipeline.*
import io.opentelemetry.context.Context
import io.opentelemetry.context.propagation.ContextPropagators import io.opentelemetry.context.propagation.ContextPropagators
import io.opentelemetry.extension.kotlin.asContextElement
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
import io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracing
import kotlinx.coroutines.InternalCoroutinesApi import io.opentelemetry.instrumentation.ktor.internal.KtorClientTracingUtil
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class KtorClientTracing internal constructor( class KtorClientTracing internal constructor(
private val instrumenter: Instrumenter<HttpRequestData, HttpResponse>, instrumenter: Instrumenter<HttpRequestData, HttpResponse>,
private val propagators: ContextPropagators, propagators: ContextPropagators
) { ) : AbstractKtorClientTracing(instrumenter, propagators) {
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)
}
companion object : HttpClientPlugin<KtorClientTracingBuilder, KtorClientTracing> { companion object : HttpClientPlugin<KtorClientTracingBuilder, KtorClientTracing> {
private val openTelemetryContextKey = AttributeKey<Context>("OpenTelemetry")
override val key = AttributeKey<KtorClientTracing>("OpenTelemetry") override val key = AttributeKey<KtorClientTracing>("OpenTelemetry")
override fun prepare(block: KtorClientTracingBuilder.() -> Unit) = KtorClientTracingBuilder().apply(block).build() override fun prepare(block: KtorClientTracingBuilder.() -> Unit) = KtorClientTracingBuilder().apply(block).build()
override fun install(plugin: KtorClientTracing, scope: HttpClient) { override fun install(plugin: KtorClientTracing, scope: HttpClient) {
installSpanCreation(plugin, scope) KtorClientTracingUtil.install(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)
}
}
} }
} }
} }

View File

@ -5,168 +5,13 @@
package io.opentelemetry.instrumentation.ktor.v2_0.client package io.opentelemetry.instrumentation.ktor.v2_0.client
import io.ktor.client.request.* import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracingBuilder
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.v2_0.InstrumentationProperties.INSTRUMENTATION_NAME import io.opentelemetry.instrumentation.ktor.v2_0.InstrumentationProperties.INSTRUMENTATION_NAME
import io.opentelemetry.instrumentation.ktor.v2_0.internal.KtorBuilderUtil
class KtorClientTracingBuilder { class KtorClientTracingBuilder : AbstractKtorClientTracingBuilder(INSTRUMENTATION_NAME) {
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)
}
internal fun build(): KtorClientTracing = KtorClientTracing( internal fun build(): KtorClientTracing = KtorClientTracing(
instrumenter = clientBuilder.build(), 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.instrumentation.testing.junit.http.HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES
import io.opentelemetry.semconv.NetworkAttributes import io.opentelemetry.semconv.NetworkAttributes
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.junit.jupiter.api.AfterAll
import java.net.URI import java.net.URI
abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest<HttpRequestBuilder>() { abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest<HttpRequestBuilder>() {
@ -27,6 +28,19 @@ abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest<HttpRequestBu
installTracing() installTracing()
} }
private val singleConnectionClient = HttpClient(CIO) {
engine {
maxConnectionsCount = 1
}
installTracing()
}
@AfterAll
fun tearDown() {
client.close()
singleConnectionClient.close()
}
abstract fun HttpClientConfig<*>.installTracing() abstract fun HttpClientConfig<*>.installTracing()
@ -67,7 +81,7 @@ abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest<HttpRequestBu
setHttpAttributes { DEFAULT_HTTP_ATTRIBUTES - setOf(NetworkAttributes.NETWORK_PROTOCOL_VERSION) } setHttpAttributes { DEFAULT_HTTP_ATTRIBUTES - setOf(NetworkAttributes.NETWORK_PROTOCOL_VERSION) }
setSingleConnectionFactory { host, port -> 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 import kotlinx.coroutines.runBlocking
class KtorHttpClientSingleConnection( class KtorHttpClientSingleConnection(
private val client: HttpClient,
private val host: String, private val host: String,
private val port: Int, private val port: Int
private val installTracing: HttpClientConfig<*>.() -> Unit,
) : SingleConnection { ) : 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 { override fun doRequest(path: String, requestHeaders: MutableMap<String, String>) = runBlocking {
val request = HttpRequestBuilder( val request = HttpRequestBuilder(
scheme = "http", 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:javaagent")
include(":instrumentation:ktor:ktor-2.0:library") include(":instrumentation:ktor:ktor-2.0:library")
include(":instrumentation:ktor:ktor-2.0:testing") 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:ktor:ktor-common:library")
include(":instrumentation:kubernetes-client-7.0:javaagent") include(":instrumentation:kubernetes-client-7.0:javaagent")
include(":instrumentation:kubernetes-client-7.0:javaagent-unit-tests") include(":instrumentation:kubernetes-client-7.0:javaagent-unit-tests")