Skip to content

Commit

Permalink
Ktor client instrumentation
Browse files Browse the repository at this point in the history
  • Loading branch information
awelless authored and aliakseiinnowise committed Mar 6, 2023
1 parent 322b15f commit 058d32d
Show file tree
Hide file tree
Showing 16 changed files with 424 additions and 11 deletions.
19 changes: 17 additions & 2 deletions instrumentation/ktor/ktor-2.0/library/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Library Instrumentation for Ktor version 2.0 and higher

This package contains libraries to help instrument Ktor. Currently, only server instrumentation is supported.
This package contains libraries to help instrument Ktor. Server and client instrumentations are supported.

## Quickstart

Expand Down Expand Up @@ -35,11 +35,26 @@ Initialize instrumentation by installing the `KtorServerTracing` feature. You mu
the feature.

```kotlin
OpenTelemetry openTelemetry = initializeOpenTelemetryForMe()
val openTelemetry: OpenTelemetry = initializeOpenTelemetryForMe()

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 = initializeOpenTelemetryForMe()

val client = HttpClient {
install(KtorClientTracing) {
setOpenTelemetry(openTelemetry)
}
}
```
8 changes: 6 additions & 2 deletions instrumentation/ktor/ktor-2.0/library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ plugins {
id("org.jetbrains.kotlin.jvm")
}

val ktorVersion = "2.0.0"

dependencies {
library("io.ktor:ktor-server-core:2.0.0")
library("io.ktor:ktor-client-core:$ktorVersion")
library("io.ktor:ktor-server-core:$ktorVersion")

implementation(project(":instrumentation:ktor:ktor-common:library"))
implementation("io.opentelemetry:opentelemetry-extension-kotlin")
Expand All @@ -16,7 +19,8 @@ dependencies {

testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")

testLibrary("io.ktor:ktor-server-netty:2.0.0")
testLibrary("io.ktor:ktor-server-netty:$ktorVersion")
testLibrary("io.ktor:ktor-client-cio:$ktorVersion")
}

tasks {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.v2_0

/**
* Common properties for both client and server instrumentations
*/
internal object InstrumentationProperties {

internal const val INSTRUMENTATION_NAME = "io.opentelemetry.ktor-2.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

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 kotlinx.coroutines.withContext

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

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

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)

if (openTelemetryContext != null) {
try {
withContext(openTelemetryContext.asContextElement()) { proceed() }
plugin.endSpan(openTelemetryContext, it.call, null)
} catch (e: Throwable) {
plugin.endSpan(openTelemetryContext, it.call, e)
throw e
}
} else {
proceed()
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.v2_0.client

import io.ktor.client.request.*
import io.ktor.client.statement.HttpResponse
import io.opentelemetry.api.OpenTelemetry
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor.alwaysClient
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor
import io.opentelemetry.instrumentation.ktor.v2_0.InstrumentationProperties.INSTRUMENTATION_NAME

class KtorClientTracingBuilder {

private var openTelemetry: OpenTelemetry? = null
private val additionalExtractors = mutableListOf<AttributesExtractor<in HttpRequestData, in HttpResponse>>()
private val httpAttributesExtractorBuilder = HttpClientAttributesExtractor.builder(
KtorHttpClientAttributesGetter,
KtorNetClientAttributesGetter,
)

fun setOpenTelemetry(openTelemetry: OpenTelemetry) {
this.openTelemetry = openTelemetry
}

fun setCapturedRequestHeaders(vararg headers: String) =
setCapturedRequestHeaders(headers.asList())

fun setCapturedRequestHeaders(headers: List<String>) {
httpAttributesExtractorBuilder.setCapturedRequestHeaders(headers)
}

fun setCapturedResponseHeaders(vararg headers: String) =
setCapturedResponseHeaders(headers.asList())

fun setCapturedResponseHeaders(headers: List<String>) {
httpAttributesExtractorBuilder.setCapturedResponseHeaders(headers)
}

fun addAttributesExtractors(vararg extractors: AttributesExtractor<in HttpRequestData, in HttpResponse>) =
addAttributesExtractors(extractors.asList())

fun addAttributesExtractors(extractors: Iterable<AttributesExtractor<in HttpRequestData, in HttpResponse>>) {
additionalExtractors += extractors
}

internal fun build(): KtorClientTracing {
val initializedOpenTelemetry = openTelemetry
?: throw IllegalArgumentException("OpenTelemetry must be set")

val instrumenterBuilder = Instrumenter.builder<HttpRequestData, HttpResponse>(
initializedOpenTelemetry,
INSTRUMENTATION_NAME,
HttpSpanNameExtractor.create(KtorHttpClientAttributesGetter),
)

val instrumenter = instrumenterBuilder
.setSpanStatusExtractor(HttpSpanStatusExtractor.create(KtorHttpClientAttributesGetter))
.addAttributesExtractor(httpAttributesExtractorBuilder.build())
.addAttributesExtractors(additionalExtractors)
.addOperationMetrics(HttpClientMetrics.get())
.buildInstrumenter(alwaysClient())

return KtorClientTracing(
instrumenter = instrumenter,
propagators = initializedOpenTelemetry.propagators,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.v2_0.client

import io.ktor.client.request.*
import io.ktor.client.statement.HttpResponse
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter

internal object KtorHttpClientAttributesGetter : HttpClientAttributesGetter<HttpRequestData, HttpResponse> {

override fun getUrl(request: HttpRequestData) =
request.url.toString()

override fun getFlavor(request: HttpRequestData, response: HttpResponse?) =
null

override fun getMethod(request: HttpRequestData) =
request.method.value

override fun getRequestHeader(request: HttpRequestData, name: String) =
request.headers.getAll(name).orEmpty()

override fun getStatusCode(request: HttpRequestData, response: HttpResponse, error: Throwable?) =
response.status.value

override fun getResponseHeader(request: HttpRequestData, response: HttpResponse, name: String) =
response.headers.getAll(name).orEmpty()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.v2_0.client

import io.ktor.client.request.HttpRequestBuilder
import io.opentelemetry.context.propagation.TextMapSetter

internal object KtorHttpHeadersSetter : TextMapSetter<HttpRequestBuilder> {

override fun set(carrier: HttpRequestBuilder?, key: String, value: String) {
carrier?.headers?.set(key, value)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.v2_0.client

import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP

internal object KtorNetClientAttributesGetter : NetClientAttributesGetter<HttpRequestData, HttpResponse> {

override fun getTransport(request: HttpRequestData, response: HttpResponse?) = IP_TCP

override fun getPeerName(request: HttpRequestData) = request.url.host

override fun getPeerPort(request: HttpRequestData) = request.url.port
}
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
package io.opentelemetry.instrumentation.ktor.v2_0.server

import io.ktor.server.request.*
import io.opentelemetry.context.propagation.TextMapGetter
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
package io.opentelemetry.instrumentation.ktor.v2_0.server

import io.ktor.server.plugins.*
import io.ktor.server.request.*
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
package io.opentelemetry.instrumentation.ktor.v2_0.server

import io.ktor.server.request.*
import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter
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
package io.opentelemetry.instrumentation.ktor.v2_0.server

import io.ktor.server.application.*
import io.ktor.server.request.*
Expand All @@ -23,6 +23,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttribut
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerMetrics
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor
import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanStatusExtractor
import io.opentelemetry.instrumentation.ktor.v2_0.InstrumentationProperties.INSTRUMENTATION_NAME
import kotlinx.coroutines.withContext

class KtorServerTracing private constructor(
Expand Down Expand Up @@ -81,7 +82,6 @@ class KtorServerTracing private constructor(
}

companion object Feature : BaseApplicationPlugin<Application, Configuration, KtorServerTracing> {
private val INSTRUMENTATION_NAME = "io.opentelemetry.ktor-2.0"

private val contextKey = AttributeKey<Context>("OpenTelemetry")
private val errorKey = AttributeKey<Throwable>("OpenTelemetryException")
Expand Down
Loading

0 comments on commit 058d32d

Please sign in to comment.