Skip to content

Commit

Permalink
Add extension functions and example for OpenTelemetry
Browse files Browse the repository at this point in the history
  • Loading branch information
marychatte authored Feb 20, 2024
2 parents 08b4b4e + a1a127a commit 4ffe6b8
Show file tree
Hide file tree
Showing 33 changed files with 1,090 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ note on [Docs and Samples Migration](https://blog.jetbrains.com/ktor/2020/09/16/
* [postgres](postgres/README.md) - An application for creating, editing and deleting articles that uses Postgres database running on Docker image as a storage.
* [mongodb](mongodb/README.md) - An application for creating, editing and deleting articles that uses Mongodb running on Docker image as a storage.
* [mvc-web](mvc-web/README.md) - An application for adding and removing wishes to wishlist that uses [FreeMarker](https://ktor.io/docs/freemarker.html) templates and Exposed.
* [opentelemetry](opentelemetry/README.md) - An application that uses Kotlin DSL to work with OpenTelemetry Ktor plugins.

## Server

Expand Down
87 changes: 87 additions & 0 deletions opentelemetry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# OpenTelemetry-Ktor Demo

[OpenTelemetry](https://opentelemetry.io/) provides support for Ktor with the `KtorClientTracing`and `KtorServerTracing`
plugins for the Ktor client and server respectively. For the source code, see
the [repository on GitHub](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/ktor).

This project contains extension functions for plugins that allow you to write code in the Ktor DSL style.

Take the following code as an example:

```kotlin
install(KtorServerTracing) {
...
addAttributeExtractor(
object : AttributesExtractor<ApplicationRequest, ApplicationResponse> {
override fun onEnd(
attributes: AttributesBuilder,
context: Context,
request: ApplicationRequest,
response: ApplicationResponse?,
error: Throwable?
) {
attributes.put("end-time", Instant.now().toEpochMilli())
}
}
)
...
}
```

Rewritten in Ktor DSL style, it looks like the following:

```kotlin
install(KtorServerTracing) {
...
attributeExtractor {
onEnd {
attributes.put("end-time", Instant.now().toEpochMilli())
}
}
...
}
```

You can find all extensions for the client plugin `KtorClientTracing` in
the [extractions](./client/src/main/kotlin/opentelemetry/ktor/example/plugins/opentelemetry/extractions/) folder. \
And you can find all extensions for the server plugin `KtorServerTracing` in
the [extractions](./server/src/main/kotlin/opentelemetry/ktor/example/plugins/opentelemetry/extractions/) folder.

## Running

**Note:** You need to have [Docker](https://www.docker.com/) installed and running to run the sample.

To run this sample, execute the following command from the `opentelemetry` directory::

```bash
./gradlew :runWithDocker
```

It will start a `Jaeger` in the docker container (`Jaeger UI` available on http://localhost:16686/search) and
then it will start a `server` on http://localhost:8080/

Then, to run the client, which will send requests to a server, you can execute the following command in
an `opentelemetry` directory:

```bash
./gradlew :client:run
```

**Note:** In this example, we use
an [Autoconfiguration OpenTelemetry instance](https://opentelemetry.io/docs/languages/java/instrumentation/#automatic-configuration),
we set environment variables `OTEL_METRICS_EXPORTER` and `OTEL_EXPORTER_OTLP_ENDPOINT`
in [build.gradle.kts](./build.gradle.kts) file.
You can find more information about these environment variables
in the [OpenTelemetry documentation](https://opentelemetry.io/docs/languages/sdk-configuration/).

Let's check what we will see in the `Jaeger UI` after running the server (with Docker) and the client:

1. We can see two our services that send opentelemetry data: `opentelemetry-ktor-sample-server`
and `opentelemetry-ktor-sample-client`, and service `jaeger-all-in-one`, it's `Jaeger` tracing some of
its components:
![img.png](images/1.png)
2. If you select `opentelemetry-ktor-sample-server` service and click on **Find traces**, you will see a list of traces:
![img.png](images/2.png)
3. If you click on one of those traces, you will be navigated to a screen providing detailed information about the
selected trace:
![img.png](images/3.png)
32 changes: 32 additions & 0 deletions opentelemetry/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
description = "OpenTelemetry-Ktor example"

plugins {
id("com.avast.gradle.docker-compose") version "0.14.0"
}

subprojects {
group = "opentelemetry.ktor.example"
version = "0.0.1"

repositories {
mavenCentral()
}
}

dockerCompose {
useComposeFiles.add("docker/docker-compose.yml")
}

tasks.register("runWithDocker") {
dependsOn("composeUp", ":server:run")
}

project(":server").setEnvironmentVariablesForOpenTelemetry()
project(":client").setEnvironmentVariablesForOpenTelemetry()

fun Project.setEnvironmentVariablesForOpenTelemetry() {
tasks.withType<JavaExec> {
environment("OTEL_METRICS_EXPORTER", "none")
environment("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317/")
}
}
28 changes: 28 additions & 0 deletions opentelemetry/client/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
val ktor_version: String by project
val logback_version: String by project
val kotlin_version: String by project
val opentelemetry_version: String by project

plugins {
kotlin("jvm") version "1.9.21"
id("io.ktor.plugin") version "2.3.8"
id("application")
}

application {
mainClass.set("opentelemetry.ktor.example.ClientKt")

val isDevelopment: Boolean = project.ext.has("development")
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
}

dependencies {
implementation(project(":shared"))

implementation("io.ktor:ktor-client-core-jvm")
implementation("io.ktor:ktor-client-cio-jvm")
implementation("io.ktor:ktor-client-websockets:$ktor_version")
implementation("ch.qos.logback:logback-classic:$logback_version")

implementation("io.opentelemetry.instrumentation:opentelemetry-ktor-2.0:$opentelemetry_version-alpha")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package opentelemetry.ktor.example

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.websocket.*
import opentelemetry.ktor.example.plugins.opentelemetry.setupClientTelemetry

suspend fun main() {
val client = HttpClient(CIO) {
install(WebSockets)

defaultRequest {
url("http://$SERVER_HOST:$SERVER_PORT")
}

setupClientTelemetry()
}

doRequests(client)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package opentelemetry.ktor.example

import io.ktor.client.*
import io.ktor.client.plugins.websocket.*
import io.ktor.client.request.*
import io.ktor.websocket.*


suspend fun doRequests(client: HttpClient) {
// For this request you can see `CUSTOM` method instead of default `HTTP` in the Jaeger UI
client.request("/known-methods") {
method = CUSTOM_METHOD
}

// For this request you can't see `CUSTOM_NOT_KNOWN` method, you can see default `HTTP` in the Jaeger UI
client.request("/known-methods") {
method = CUSTOM_METHOD_NOT_KNOWN
}

// You can see tags `http.request.header.accept` and `http.response.header.content_type` for all requests
// in the Jaeger UI and also `http.response.header.custom_header` for this request
client.get("/captured-headers")

// For this request you can see tag `error=true` and `Error` icon only for server trace in the Jaeger UI
client.get("/span-status-extractor")

// For this request you can see tag `span.kind=producer` only for server trace in the Jaeger UI
client.post("/span-kind-extractor")

// You can see attribute `start-time` and `end-time` in the Jaeger UI for all requests
client.get("/attribute-extractor")

// For this request you can see several spans and events only for server trace in the Jaeger UI
client.get("/opentelemetry/tracer")

// For this request you can see several events only for server trace in the Jaeger UI
client.ws("/opentelemetry/websocket") {
send(Frame.Text("Hello, world!"))
repeat(10) {
send(incoming.receive())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package opentelemetry.ktor.example.plugins.opentelemetry.extractions

import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.opentelemetry.api.common.AttributesBuilder
import io.opentelemetry.context.Context
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder

// addAttributeExtractor
fun KtorClientTracingBuilder.attributeExtractor(
extractorBuilder: ExtractorBuilder.() -> Unit = {}
) {
val builder = ExtractorBuilder().apply(extractorBuilder).build()
addAttributesExtractors(
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?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package opentelemetry.ktor.example.plugins.opentelemetry.extractions

import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder

// setCapturedRequestHeaders
fun KtorClientTracingBuilder.capturedRequestHeaders(vararg headers: String) {
capturedRequestHeaders(headers.asIterable())
}

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

// setCapturedResponseHeaders
fun KtorClientTracingBuilder.capturedResponseHeaders(vararg headers: String) {
capturedResponseHeaders(headers.asIterable())
}

fun KtorClientTracingBuilder.capturedResponseHeaders(headers: Iterable<String>) {
setCapturedResponseHeaders(headers.toList())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package opentelemetry.ktor.example.plugins.opentelemetry.extractions

import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder

// setEmitExperimentalHttpClientMetrics
fun KtorClientTracingBuilder.emitExperimentalHttpClientMetrics() {
setEmitExperimentalHttpClientMetrics(true)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package opentelemetry.ktor.example.plugins.opentelemetry.extractions

import io.ktor.http.*
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder

// setKnownMethods
fun KtorClientTracingBuilder.knownMethods(vararg methods: HttpMethod) {
knownMethods(methods.asIterable())
}

fun KtorClientTracingBuilder.knownMethods(methods: Iterable<HttpMethod>) {
setKnownMethods(methods.map { it.value }.toSet())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package opentelemetry.ktor.example.plugins.opentelemetry

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.http.*
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracing
import opentelemetry.ktor.example.CUSTOM_HEADER
import opentelemetry.ktor.example.CUSTOM_METHOD
import opentelemetry.ktor.example.getOpenTelemetry
import opentelemetry.ktor.example.plugins.opentelemetry.extractions.*

/**
* Install OpenTelemetry on the client.
* You can see usages of new extension functions for [KtorClientTracing].
*/
fun HttpClientConfig<CIOEngineConfig>.setupClientTelemetry() {
val openTelemetry = getOpenTelemetry(serviceName = "opentelemetry-ktor-sample-client")
install(KtorClientTracing) {
setOpenTelemetry(openTelemetry)

emitExperimentalHttpClientMetrics()

knownMethods(HttpMethod.DefaultMethods + CUSTOM_METHOD)
capturedRequestHeaders(HttpHeaders.Accept)
capturedResponseHeaders(HttpHeaders.ContentType, CUSTOM_HEADER)

attributeExtractor {
onStart {
attributes.put("start-time", System.currentTimeMillis())
}
onEnd {
attributes.put("end-time", System.currentTimeMillis())
}
}
}
}
8 changes: 8 additions & 0 deletions opentelemetry/docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# This file is used to start the Jaeger all-in-one container
version: '3.7'
services:
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "4317:4317" # OTLP gRPC receiver
- "16686:16686" # Jaeger UI
Loading

0 comments on commit 4ffe6b8

Please sign in to comment.