Skip to content

Commit

Permalink
Rename ktor tracing to ktor telemetry
Browse files Browse the repository at this point in the history
  • Loading branch information
trask committed Dec 9, 2024
1 parent d26c1f6 commit 04d72e2
Show file tree
Hide file tree
Showing 33 changed files with 978 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.ktor.v1_0

import io.ktor.application.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.util.*
import io.ktor.util.pipeline.*
import io.opentelemetry.api.OpenTelemetry
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 kotlinx.coroutines.withContext

class KtorServerTelemetry private constructor(
private val instrumenter: Instrumenter<ApplicationRequest, ApplicationResponse>,
) {

class Configuration {
internal lateinit var builder: DefaultHttpServerInstrumenterBuilder<ApplicationRequest, ApplicationResponse>

internal var spanKindExtractor:
(SpanKindExtractor<ApplicationRequest>) -> SpanKindExtractor<ApplicationRequest> = { a -> a }

fun setOpenTelemetry(openTelemetry: OpenTelemetry) {
this.builder =
DefaultHttpServerInstrumenterBuilder.create(
INSTRUMENTATION_NAME,
openTelemetry,
KtorHttpServerAttributesGetter.INSTANCE
)
}

fun setStatusExtractor(
extractor: (SpanStatusExtractor<in ApplicationRequest, in ApplicationResponse>) -> SpanStatusExtractor<in ApplicationRequest, in ApplicationResponse>
) {
builder.setStatusExtractor { prevExtractor ->
SpanStatusExtractor { spanStatusBuilder: SpanStatusBuilder,
request: ApplicationRequest,
response: ApplicationResponse?,
throwable: Throwable? ->
extractor(prevExtractor).extract(spanStatusBuilder, request, response, throwable)
}
}
}

fun setSpanKindExtractor(extractor: (SpanKindExtractor<ApplicationRequest>) -> SpanKindExtractor<ApplicationRequest>) {
this.spanKindExtractor = extractor
}

fun addAttributeExtractor(extractor: AttributesExtractor<in ApplicationRequest, in ApplicationResponse>) {
builder.addAttributesExtractor(extractor)
}

fun setCapturedRequestHeaders(requestHeaders: List<String>) {
builder.setCapturedRequestHeaders(requestHeaders)
}

fun setCapturedResponseHeaders(responseHeaders: List<String>) {
builder.setCapturedResponseHeaders(responseHeaders)
}

fun setKnownMethods(knownMethods: Set<String>) {
builder.setKnownMethods(knownMethods)
}

internal fun isOpenTelemetryInitialized(): Boolean = this::builder.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 : ApplicationFeature<Application, Configuration, KtorServerTelemetry> {
private const val INSTRUMENTATION_NAME = "io.opentelemetry.ktor-1.0"

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

override val key: AttributeKey<KtorServerTelemetry> = AttributeKey("OpenTelemetry")

override fun install(pipeline: Application, configure: Configuration.() -> Unit): KtorServerTelemetry {
val configuration = Configuration().apply(configure)

if (!configuration.isOpenTelemetryInitialized()) {
throw IllegalArgumentException("OpenTelemetry must be set")
}

val instrumenter = InstrumenterUtil.buildUpstreamInstrumenter(
configuration.builder.instrumenterBuilder(),
ApplicationRequestGetter,
configuration.spanKindExtractor(SpanKindExtractor.alwaysServer())
)

val feature = KtorServerTelemetry(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 ->
val context = call.attributes.getOrNull(contextKey)
if (context != null) {
HttpServerRoute.update(context, HttpServerRouteSource.SERVER, { _, arg -> arg.route.parent.toString() }, call)
}
}

return feature
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource
import kotlinx.coroutines.withContext

@Deprecated("Use KtorServerTelemetry instead", ReplaceWith("KtorServerTelemetry"))
class KtorServerTracing private constructor(
private val instrumenter: Instrumenter<ApplicationRequest, ApplicationResponse>,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class KtorServerSpanKindExtractorTest : AbstractHttpServerUsingTest<ApplicationE

override fun setupServer(): ApplicationEngine {
return embeddedServer(Netty, port = port) {
install(KtorServerTracing) {
install(KtorServerTelemetry) {
setOpenTelemetry(testing.openTelemetry)
setSpanKindExtractor {
SpanKindExtractor { req ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTes
class KtorTestUtil {
companion object {
fun installOpenTelemetry(application: Application, openTelemetry: OpenTelemetry) {
application.install(KtorServerTracing) {
application.install(KtorServerTelemetry) {
setOpenTelemetry(openTelemetry)
setCapturedRequestHeaders(listOf(AbstractHttpServerTest.TEST_REQUEST_HEADER))
setCapturedResponseHeaders(listOf(AbstractHttpServerTest.TEST_RESPONSE_HEADER))
Expand Down
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 AbstractKtorClientTelemetry(
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,121 @@
/*
* 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 AbstractKtorClientTelemetryBuilder(
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
}

fun capturedRequestHeaders(vararg headers: String) = capturedRequestHeaders(headers.asIterable())

fun capturedRequestHeaders(headers: Iterable<String>) {
clientBuilder.setCapturedRequestHeaders(headers.toList())
}

fun capturedResponseHeaders(vararg headers: String) = capturedResponseHeaders(headers.asIterable())

fun capturedResponseHeaders(headers: Iterable<String>) {
clientBuilder.setCapturedResponseHeaders(headers.toList())
}

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

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

/**
* Can be used via the unstable method {@link
* Experimental#setEmitExperimentalHttpClientMetrics(AbstractKtorClientTelemetryBuilder, boolean)}.
*/
internal fun emitExperimentalHttpClientMetrics() {
clientBuilder.setEmitExperimentalHttpClientMetrics(true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.opentelemetry.context.Context
import io.opentelemetry.context.propagation.ContextPropagators
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter

@Deprecated("Use AbstractKtorClientTelemetry instead", ReplaceWith("AbstractKtorClientTelemetry"))
abstract class AbstractKtorClientTracing(
private val instrumenter: Instrumenter<HttpRequestData, HttpResponse>,
private val propagators: ContextPropagators,
Expand Down
Loading

0 comments on commit 04d72e2

Please sign in to comment.