Add ktor 3 instrumentation (#12562)
This commit is contained in:
parent
fdfb764c76
commit
76c2d2d8f3
|
@ -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 |
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.*
|
|
@ -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
|
|
@ -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>
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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")
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue