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] |
|
||||
| [JSP](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/jsp/package-summary.html) | 2.3+ | N/A | Controller Spans [3] |
|
||||
| [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) | 1.0+ | N/A | Context propagation |
|
||||
| [Ktor](https://github.com/ktorio/ktor) | 1.0+ | [opentelemetry-ktor-1.0](../instrumentation/ktor/ktor-1.0/library),<br>[opentelemetry-ktor-2.0](../instrumentation/ktor/ktor-2.0/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] |
|
||||
| [Ktor](https://github.com/ktorio/ktor) | 1.0+ | [opentelemetry-ktor-1.0](../instrumentation/ktor/ktor-1.0/library),<br>[opentelemetry-ktor-2.0](../instrumentation/ktor/ktor-2.0/library),<br>[opentelemetry-ktor-3.0](../instrumentation/ktor/ktor-3.0/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] |
|
||||
| [Kubernetes Client](https://github.com/kubernetes-client/java) | 7.0+ | N/A | [HTTP Client Spans] |
|
||||
| [Lettuce](https://github.com/lettuce-io/lettuce-core) | 4.0+ | [opentelemetry-lettuce-5.1](../instrumentation/lettuce/lettuce-5.1/library) | [Database Client Spans] |
|
||||
| [Log4j 1](https://logging.apache.org/log4j/1.2/) | 1.2+ | N/A | none |
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.ktor.v2_0.client
|
||||
package io.opentelemetry.instrumentation.ktor.client
|
||||
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.ktor.v2_0.client
|
||||
package io.opentelemetry.instrumentation.ktor.client
|
||||
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.opentelemetry.context.propagation.TextMapSetter
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.ktor.v2_0.internal
|
||||
package io.opentelemetry.instrumentation.ktor.internal
|
||||
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
|
@ -11,14 +11,16 @@ import io.ktor.server.request.*
|
|||
import io.ktor.server.response.*
|
||||
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder
|
||||
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpServerInstrumenterBuilder
|
||||
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder
|
||||
import io.opentelemetry.instrumentation.ktor.v2_0.server.KtorServerTracing
|
||||
import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracingBuilder
|
||||
import io.opentelemetry.instrumentation.ktor.server.AbstractKtorServerTracingBuilder
|
||||
|
||||
/**
|
||||
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
|
||||
* any time.
|
||||
*/
|
||||
object KtorBuilderUtil {
|
||||
lateinit var clientBuilderExtractor: (KtorClientTracingBuilder) -> DefaultHttpClientInstrumenterBuilder<HttpRequestData, HttpResponse>
|
||||
lateinit var serverBuilderExtractor: (KtorServerTracing.Configuration) -> DefaultHttpServerInstrumenterBuilder<ApplicationRequest, ApplicationResponse>
|
||||
lateinit var clientBuilderExtractor: (AbstractKtorClientTracingBuilder) -> DefaultHttpClientInstrumenterBuilder<HttpRequestData, HttpResponse>
|
||||
lateinit var serverBuilderExtractor: (
|
||||
AbstractKtorServerTracingBuilder
|
||||
) -> DefaultHttpServerInstrumenterBuilder<ApplicationRequest, ApplicationResponse>
|
||||
}
|
|
@ -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
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.ktor.v2_0.server
|
||||
package io.opentelemetry.instrumentation.ktor.server
|
||||
|
||||
import io.ktor.server.request.*
|
||||
import io.opentelemetry.context.propagation.TextMapGetter
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.ktor.v2_0.server
|
||||
package io.opentelemetry.instrumentation.ktor.server
|
||||
|
||||
import io.ktor.server.plugins.*
|
||||
import io.ktor.server.request.*
|
||||
|
@ -13,7 +13,7 @@ import io.opentelemetry.instrumentation.ktor.isIpAddress
|
|||
|
||||
internal enum class KtorHttpServerAttributesGetter :
|
||||
HttpServerAttributesGetter<ApplicationRequest, ApplicationResponse> {
|
||||
INSTANCE, ;
|
||||
INSTANCE;
|
||||
|
||||
override fun getHttpRequestMethod(request: ApplicationRequest): String {
|
||||
return request.httpMethod.value
|
|
@ -7,10 +7,22 @@ plugins {
|
|||
|
||||
muzzle {
|
||||
pass {
|
||||
group.set("org.jetbrains.kotlinx")
|
||||
module.set("ktor-server-core")
|
||||
versions.set("[2.0.0,)")
|
||||
group.set("io.ktor")
|
||||
module.set("ktor-client-core")
|
||||
versions.set("[2.0.0,3.0.0)")
|
||||
assertInverse.set(true)
|
||||
excludeInstrumentationName("ktor-server")
|
||||
// missing dependencies
|
||||
skip("1.1.0", "1.1.1", "1.1.5")
|
||||
}
|
||||
pass {
|
||||
group.set("io.ktor")
|
||||
module.set("ktor-server-core")
|
||||
versions.set("[2.0.0,3.0.0)")
|
||||
assertInverse.set(true)
|
||||
excludeInstrumentationName("ktor-client")
|
||||
// missing dependencies
|
||||
skip("1.1.0", "1.1.1")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,6 +37,7 @@ dependencies {
|
|||
compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
|
||||
testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent"))
|
||||
testInstrumentation(project(":instrumentation:ktor:ktor-3.0:javaagent"))
|
||||
|
||||
testImplementation(project(":instrumentation:ktor:ktor-2.0:testing"))
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
|
|
|
@ -13,9 +13,9 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
|
|||
import io.ktor.client.HttpClientConfig;
|
||||
import io.ktor.client.engine.HttpClientEngineConfig;
|
||||
import io.opentelemetry.api.GlobalOpenTelemetry;
|
||||
import io.opentelemetry.instrumentation.ktor.internal.KtorBuilderUtil;
|
||||
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracing;
|
||||
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder;
|
||||
import io.opentelemetry.instrumentation.ktor.v2_0.internal.KtorBuilderUtil;
|
||||
import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig;
|
||||
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
|
||||
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
|
||||
|
|
|
@ -5,12 +5,14 @@
|
|||
|
||||
package io.opentelemetry.javaagent.instrumentation.ktor.v2_0;
|
||||
|
||||
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
|
||||
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
|
||||
import java.util.List;
|
||||
import net.bytebuddy.matcher.ElementMatcher;
|
||||
|
||||
@AutoService(InstrumentationModule.class)
|
||||
public class KtorClientInstrumentationModule extends InstrumentationModule {
|
||||
|
@ -24,6 +26,12 @@ public class KtorClientInstrumentationModule extends InstrumentationModule {
|
|||
return className.startsWith("io.opentelemetry.extension.kotlin.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
|
||||
// removed in ktor 3
|
||||
return hasClassesNamed("io.ktor.client.engine.HttpClientJvmEngine");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TypeInstrumentation> typeInstrumentations() {
|
||||
return singletonList(new HttpClientInstrumentation());
|
||||
|
|
|
@ -11,8 +11,9 @@ import static net.bytebuddy.matcher.ElementMatchers.named;
|
|||
import io.ktor.server.application.Application;
|
||||
import io.ktor.server.application.ApplicationPluginKt;
|
||||
import io.opentelemetry.api.GlobalOpenTelemetry;
|
||||
import io.opentelemetry.instrumentation.ktor.v2_0.internal.KtorBuilderUtil;
|
||||
import io.opentelemetry.instrumentation.ktor.v2_0.server.KtorServerTracing;
|
||||
import io.opentelemetry.instrumentation.ktor.internal.KtorBuilderUtil;
|
||||
import io.opentelemetry.instrumentation.ktor.server.AbstractKtorServerTracingBuilder;
|
||||
import io.opentelemetry.instrumentation.ktor.v2_0.server.KtorServerTracingBuilderKt;
|
||||
import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig;
|
||||
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
|
||||
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
|
||||
|
@ -39,19 +40,18 @@ public class ServerInstrumentation implements TypeInstrumentation {
|
|||
|
||||
@Advice.OnMethodExit
|
||||
public static void onExit(@Advice.FieldValue("_applicationInstance") Application application) {
|
||||
ApplicationPluginKt.install(application, KtorServerTracing.Feature, new SetupFunction());
|
||||
ApplicationPluginKt.install(
|
||||
application, KtorServerTracingBuilderKt.getKtorServerTracing(), new SetupFunction());
|
||||
}
|
||||
}
|
||||
|
||||
public static class SetupFunction
|
||||
implements Function1<KtorServerTracing.Configuration, kotlin.Unit> {
|
||||
implements Function1<AbstractKtorServerTracingBuilder, kotlin.Unit> {
|
||||
|
||||
@Override
|
||||
public Unit invoke(KtorServerTracing.Configuration configuration) {
|
||||
configuration.setOpenTelemetry(GlobalOpenTelemetry.get());
|
||||
KtorBuilderUtil.serverBuilderExtractor
|
||||
.invoke(configuration)
|
||||
.configure(AgentCommonConfig.get());
|
||||
public Unit invoke(AbstractKtorServerTracingBuilder builder) {
|
||||
builder.setOpenTelemetry(GlobalOpenTelemetry.get());
|
||||
KtorBuilderUtil.serverBuilderExtractor.invoke(builder).configure(AgentCommonConfig.get());
|
||||
return kotlin.Unit.INSTANCE;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ dependencies {
|
|||
library("io.ktor:ktor-client-core:$ktorVersion")
|
||||
library("io.ktor:ktor-server-core:$ktorVersion")
|
||||
|
||||
implementation(project(":instrumentation:ktor:ktor-common:library"))
|
||||
api(project(":instrumentation:ktor:ktor-2-common:library"))
|
||||
implementation("io.opentelemetry:opentelemetry-extension-kotlin")
|
||||
|
||||
compileOnly("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
|
|
|
@ -6,116 +6,28 @@
|
|||
package io.opentelemetry.instrumentation.ktor.v2_0.client
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.util.*
|
||||
import io.ktor.util.pipeline.*
|
||||
import io.opentelemetry.context.Context
|
||||
import io.opentelemetry.context.propagation.ContextPropagators
|
||||
import io.opentelemetry.extension.kotlin.asContextElement
|
||||
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
|
||||
import io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount
|
||||
import kotlinx.coroutines.InternalCoroutinesApi
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracing
|
||||
import io.opentelemetry.instrumentation.ktor.internal.KtorClientTracingUtil
|
||||
|
||||
class KtorClientTracing internal constructor(
|
||||
private val instrumenter: Instrumenter<HttpRequestData, HttpResponse>,
|
||||
private val propagators: ContextPropagators,
|
||||
) {
|
||||
|
||||
private fun createSpan(requestBuilder: HttpRequestBuilder): Context? {
|
||||
val parentContext = Context.current()
|
||||
val requestData = requestBuilder.build()
|
||||
|
||||
return if (instrumenter.shouldStart(parentContext, requestData)) {
|
||||
instrumenter.start(parentContext, requestData)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateRequestHeaders(requestBuilder: HttpRequestBuilder, context: Context) {
|
||||
propagators.textMapPropagator.inject(context, requestBuilder, KtorHttpHeadersSetter)
|
||||
}
|
||||
|
||||
private fun endSpan(context: Context, call: HttpClientCall, error: Throwable?) {
|
||||
endSpan(context, HttpRequestBuilder().takeFrom(call.request), call.response, error)
|
||||
}
|
||||
|
||||
private fun endSpan(context: Context, requestBuilder: HttpRequestBuilder, response: HttpResponse?, error: Throwable?) {
|
||||
instrumenter.end(context, requestBuilder.build(), response, error)
|
||||
}
|
||||
instrumenter: Instrumenter<HttpRequestData, HttpResponse>,
|
||||
propagators: ContextPropagators
|
||||
) : AbstractKtorClientTracing(instrumenter, propagators) {
|
||||
|
||||
companion object : HttpClientPlugin<KtorClientTracingBuilder, KtorClientTracing> {
|
||||
|
||||
private val openTelemetryContextKey = AttributeKey<Context>("OpenTelemetry")
|
||||
|
||||
override val key = AttributeKey<KtorClientTracing>("OpenTelemetry")
|
||||
|
||||
override fun prepare(block: KtorClientTracingBuilder.() -> Unit) = KtorClientTracingBuilder().apply(block).build()
|
||||
|
||||
override fun install(plugin: KtorClientTracing, scope: HttpClient) {
|
||||
installSpanCreation(plugin, scope)
|
||||
installSpanEnd(plugin, scope)
|
||||
}
|
||||
|
||||
private fun installSpanCreation(plugin: KtorClientTracing, scope: HttpClient) {
|
||||
val initializeRequestPhase = PipelinePhase("OpenTelemetryInitializeRequest")
|
||||
scope.requestPipeline.insertPhaseAfter(HttpRequestPipeline.State, initializeRequestPhase)
|
||||
|
||||
scope.requestPipeline.intercept(initializeRequestPhase) {
|
||||
val openTelemetryContext = HttpClientRequestResendCount.initialize(Context.current())
|
||||
withContext(openTelemetryContext.asContextElement()) { proceed() }
|
||||
}
|
||||
|
||||
val createSpanPhase = PipelinePhase("OpenTelemetryCreateSpan")
|
||||
scope.sendPipeline.insertPhaseAfter(HttpSendPipeline.State, createSpanPhase)
|
||||
|
||||
scope.sendPipeline.intercept(createSpanPhase) {
|
||||
val requestBuilder = context
|
||||
val openTelemetryContext = plugin.createSpan(requestBuilder)
|
||||
|
||||
if (openTelemetryContext != null) {
|
||||
try {
|
||||
requestBuilder.attributes.put(openTelemetryContextKey, openTelemetryContext)
|
||||
plugin.populateRequestHeaders(requestBuilder, openTelemetryContext)
|
||||
|
||||
withContext(openTelemetryContext.asContextElement()) { proceed() }
|
||||
} catch (e: Throwable) {
|
||||
plugin.endSpan(openTelemetryContext, requestBuilder, null, e)
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
proceed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(InternalCoroutinesApi::class)
|
||||
private fun installSpanEnd(plugin: KtorClientTracing, scope: HttpClient) {
|
||||
val endSpanPhase = PipelinePhase("OpenTelemetryEndSpan")
|
||||
scope.receivePipeline.insertPhaseBefore(HttpReceivePipeline.State, endSpanPhase)
|
||||
|
||||
scope.receivePipeline.intercept(endSpanPhase) {
|
||||
val openTelemetryContext = it.call.attributes.getOrNull(openTelemetryContextKey)
|
||||
openTelemetryContext ?: return@intercept
|
||||
|
||||
scope.launch {
|
||||
val job = it.call.coroutineContext.job
|
||||
job.join()
|
||||
val cause = if (!job.isCancelled) {
|
||||
null
|
||||
} else {
|
||||
kotlin.runCatching { job.getCancellationException() }.getOrNull()
|
||||
}
|
||||
|
||||
plugin.endSpan(openTelemetryContext, it.call, cause)
|
||||
}
|
||||
}
|
||||
KtorClientTracingUtil.install(plugin, scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,168 +5,13 @@
|
|||
|
||||
package io.opentelemetry.instrumentation.ktor.v2_0.client
|
||||
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.opentelemetry.api.OpenTelemetry
|
||||
import io.opentelemetry.api.common.AttributesBuilder
|
||||
import io.opentelemetry.context.Context
|
||||
import io.opentelemetry.instrumentation.api.incubator.builder.internal.DefaultHttpClientInstrumenterBuilder
|
||||
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor
|
||||
import io.opentelemetry.instrumentation.ktor.client.AbstractKtorClientTracingBuilder
|
||||
import io.opentelemetry.instrumentation.ktor.v2_0.InstrumentationProperties.INSTRUMENTATION_NAME
|
||||
import io.opentelemetry.instrumentation.ktor.v2_0.internal.KtorBuilderUtil
|
||||
|
||||
class KtorClientTracingBuilder {
|
||||
companion object {
|
||||
init {
|
||||
KtorBuilderUtil.clientBuilderExtractor = { it.clientBuilder }
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var openTelemetry: OpenTelemetry
|
||||
private lateinit var clientBuilder: DefaultHttpClientInstrumenterBuilder<HttpRequestData, HttpResponse>
|
||||
|
||||
fun setOpenTelemetry(openTelemetry: OpenTelemetry) {
|
||||
this.openTelemetry = openTelemetry
|
||||
this.clientBuilder = DefaultHttpClientInstrumenterBuilder.create(
|
||||
INSTRUMENTATION_NAME,
|
||||
openTelemetry,
|
||||
KtorHttpClientAttributesGetter
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Please use method `capturedRequestHeaders`",
|
||||
ReplaceWith("capturedRequestHeaders(headers.asIterable())")
|
||||
)
|
||||
fun setCapturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())
|
||||
|
||||
@Deprecated(
|
||||
"Please use method `capturedRequestHeaders`",
|
||||
ReplaceWith("capturedRequestHeaders(headers)")
|
||||
)
|
||||
fun setCapturedRequestHeaders(headers: List<String>) = capturedRequestHeaders(headers)
|
||||
|
||||
fun capturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())
|
||||
|
||||
fun capturedRequestHeaders(headers: Iterable<String>) {
|
||||
clientBuilder.setCapturedRequestHeaders(headers.toList())
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Please use method `capturedResponseHeaders`",
|
||||
ReplaceWith("capturedResponseHeaders(headers.asIterable())")
|
||||
)
|
||||
fun setCapturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())
|
||||
|
||||
@Deprecated(
|
||||
"Please use method `capturedResponseHeaders`",
|
||||
ReplaceWith("capturedResponseHeaders(headers)")
|
||||
)
|
||||
fun setCapturedResponseHeaders(headers: List<String>) = capturedResponseHeaders(headers)
|
||||
|
||||
fun capturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())
|
||||
|
||||
fun capturedResponseHeaders(headers: Iterable<String>) {
|
||||
clientBuilder.setCapturedResponseHeaders(headers.toList())
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Please use method `knownMethods`",
|
||||
ReplaceWith("knownMethods(knownMethods)")
|
||||
)
|
||||
fun setKnownMethods(knownMethods: Set<String>) = knownMethods(knownMethods)
|
||||
|
||||
fun knownMethods(vararg methods: String) = knownMethods(methods.asIterable())
|
||||
|
||||
fun knownMethods(vararg methods: HttpMethod) = knownMethods(methods.asIterable())
|
||||
|
||||
@JvmName("knownMethodsJvm")
|
||||
fun knownMethods(methods: Iterable<HttpMethod>) = knownMethods(methods.map { it.value })
|
||||
|
||||
fun knownMethods(methods: Iterable<String>) {
|
||||
clientBuilder.setKnownMethods(methods.toSet())
|
||||
}
|
||||
|
||||
@Deprecated("Please use method `attributeExtractor`")
|
||||
fun addAttributesExtractors(vararg extractors: AttributesExtractor<in HttpRequestData, in HttpResponse>) = addAttributesExtractors(extractors.asList())
|
||||
|
||||
@Deprecated("Please use method `attributeExtractor`")
|
||||
fun addAttributesExtractors(extractors: Iterable<AttributesExtractor<in HttpRequestData, in HttpResponse>>) {
|
||||
extractors.forEach {
|
||||
attributeExtractor {
|
||||
onStart { it.onStart(attributes, parentContext, request) }
|
||||
onEnd { it.onEnd(attributes, parentContext, request, response, error) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun attributeExtractor(extractorBuilder: ExtractorBuilder.() -> Unit = {}) {
|
||||
val builder = ExtractorBuilder().apply(extractorBuilder).build()
|
||||
this.clientBuilder.addAttributeExtractor(
|
||||
object : AttributesExtractor<HttpRequestData, HttpResponse> {
|
||||
override fun onStart(attributes: AttributesBuilder, parentContext: Context, request: HttpRequestData) {
|
||||
builder.onStart(OnStartData(attributes, parentContext, request))
|
||||
}
|
||||
|
||||
override fun onEnd(attributes: AttributesBuilder, context: Context, request: HttpRequestData, response: HttpResponse?, error: Throwable?) {
|
||||
builder.onEnd(OnEndData(attributes, context, request, response, error))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
class ExtractorBuilder {
|
||||
private var onStart: OnStartData.() -> Unit = {}
|
||||
private var onEnd: OnEndData.() -> Unit = {}
|
||||
|
||||
fun onStart(block: OnStartData.() -> Unit) {
|
||||
onStart = block
|
||||
}
|
||||
|
||||
fun onEnd(block: OnEndData.() -> Unit) {
|
||||
onEnd = block
|
||||
}
|
||||
|
||||
internal fun build(): Extractor {
|
||||
return Extractor(onStart, onEnd)
|
||||
}
|
||||
}
|
||||
|
||||
internal class Extractor(val onStart: OnStartData.() -> Unit, val onEnd: OnEndData.() -> Unit)
|
||||
|
||||
data class OnStartData(
|
||||
val attributes: AttributesBuilder,
|
||||
val parentContext: Context,
|
||||
val request: HttpRequestData
|
||||
)
|
||||
|
||||
data class OnEndData(
|
||||
val attributes: AttributesBuilder,
|
||||
val parentContext: Context,
|
||||
val request: HttpRequestData,
|
||||
val response: HttpResponse?,
|
||||
val error: Throwable?
|
||||
)
|
||||
|
||||
/**
|
||||
* Configures the instrumentation to emit experimental HTTP client metrics.
|
||||
*
|
||||
* @param emitExperimentalHttpClientMetrics `true` if the experimental HTTP client metrics are to be emitted.
|
||||
*/
|
||||
@Deprecated("Please use method `emitExperimentalHttpClientMetrics`")
|
||||
fun setEmitExperimentalHttpClientMetrics(emitExperimentalHttpClientMetrics: Boolean) {
|
||||
if (emitExperimentalHttpClientMetrics) {
|
||||
emitExperimentalHttpClientMetrics()
|
||||
}
|
||||
}
|
||||
|
||||
fun emitExperimentalHttpClientMetrics() {
|
||||
clientBuilder.setEmitExperimentalHttpClientMetrics(true)
|
||||
}
|
||||
class KtorClientTracingBuilder : AbstractKtorClientTracingBuilder(INSTRUMENTATION_NAME) {
|
||||
|
||||
internal fun build(): KtorClientTracing = KtorClientTracing(
|
||||
instrumenter = clientBuilder.build(),
|
||||
propagators = openTelemetry.propagators,
|
||||
propagators = getOpenTelemetry().propagators,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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.semconv.NetworkAttributes
|
||||
import kotlinx.coroutines.*
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import java.net.URI
|
||||
|
||||
abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest<HttpRequestBuilder>() {
|
||||
|
@ -27,6 +28,19 @@ abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest<HttpRequestBu
|
|||
|
||||
installTracing()
|
||||
}
|
||||
private val singleConnectionClient = HttpClient(CIO) {
|
||||
engine {
|
||||
maxConnectionsCount = 1
|
||||
}
|
||||
|
||||
installTracing()
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDown() {
|
||||
client.close()
|
||||
singleConnectionClient.close()
|
||||
}
|
||||
|
||||
abstract fun HttpClientConfig<*>.installTracing()
|
||||
|
||||
|
@ -67,7 +81,7 @@ abstract class AbstractKtorHttpClientTest : AbstractHttpClientTest<HttpRequestBu
|
|||
setHttpAttributes { DEFAULT_HTTP_ATTRIBUTES - setOf(NetworkAttributes.NETWORK_PROTOCOL_VERSION) }
|
||||
|
||||
setSingleConnectionFactory { host, port ->
|
||||
KtorHttpClientSingleConnection(host, port) { installTracing() }
|
||||
KtorHttpClientSingleConnection(singleConnectionClient, host, port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,23 +12,11 @@ import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection
|
|||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class KtorHttpClientSingleConnection(
|
||||
private val client: HttpClient,
|
||||
private val host: String,
|
||||
private val port: Int,
|
||||
private val installTracing: HttpClientConfig<*>.() -> Unit,
|
||||
private val port: Int
|
||||
) : SingleConnection {
|
||||
|
||||
private val client: HttpClient
|
||||
|
||||
init {
|
||||
val engine = CIO.create {
|
||||
maxConnectionsCount = 1
|
||||
}
|
||||
|
||||
client = HttpClient(engine) {
|
||||
installTracing()
|
||||
}
|
||||
}
|
||||
|
||||
override fun doRequest(path: String, requestHeaders: MutableMap<String, String>) = runBlocking {
|
||||
val request = HttpRequestBuilder(
|
||||
scheme = "http",
|
||||
|
|
|
@ -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:library")
|
||||
include(":instrumentation:ktor:ktor-2.0:testing")
|
||||
include(":instrumentation:ktor:ktor-2-common:library")
|
||||
include(":instrumentation:ktor:ktor-3.0:javaagent")
|
||||
include(":instrumentation:ktor:ktor-3.0:library")
|
||||
include(":instrumentation:ktor:ktor-3.0:testing")
|
||||
include(":instrumentation:ktor:ktor-common:library")
|
||||
include(":instrumentation:kubernetes-client-7.0:javaagent")
|
||||
include(":instrumentation:kubernetes-client-7.0:javaagent-unit-tests")
|
||||
|
|
Loading…
Reference in New Issue