Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ktor 3 instrumentation #12562

Merged
merged 11 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/supported-libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ These are the supported libraries and frameworks:
| [Jodd Http](https://http.jodd.org/) | 4.2+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] |
| [JSP](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/jsp/package-summary.html) | 2.3+ | N/A | Controller Spans [3] |
| [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) | 1.0+ | N/A | Context propagation |
| [Ktor](https://github.com/ktorio/ktor) | 1.0+ | [opentelemetry-ktor-1.0](../instrumentation/ktor/ktor-1.0/library),<br>[opentelemetry-ktor-2.0](../instrumentation/ktor/ktor-2.0/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] |
| [Ktor](https://github.com/ktorio/ktor) | 1.0+ | [opentelemetry-ktor-1.0](../instrumentation/ktor/ktor-1.0/library),<br>[opentelemetry-ktor-2.0](../instrumentation/ktor/ktor-2.0/library),<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 |
Expand Down
2 changes: 1 addition & 1 deletion instrumentation/ktor/ktor-1.0/library/README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
22 changes: 22 additions & 0 deletions instrumentation/ktor/ktor-2-common/library/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,24 @@
* 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.*
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.AbstractKtorServerTracing

/**
* 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: (
AbstractKtorServerTracing.Configuration
) -> DefaultHttpServerInstrumenterBuilder<ApplicationRequest, ApplicationResponse>
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
Loading
Loading