Skip to content

Commit 4ffe6b8

Browse files
authored
Add extension functions and example for OpenTelemetry
2 parents 08b4b4e + a1a127a commit 4ffe6b8

File tree

33 files changed

+1090
-0
lines changed

33 files changed

+1090
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ note on [Docs and Samples Migration](https://blog.jetbrains.com/ktor/2020/09/16/
2525
* [postgres](postgres/README.md) - An application for creating, editing and deleting articles that uses Postgres database running on Docker image as a storage.
2626
* [mongodb](mongodb/README.md) - An application for creating, editing and deleting articles that uses Mongodb running on Docker image as a storage.
2727
* [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.
28+
* [opentelemetry](opentelemetry/README.md) - An application that uses Kotlin DSL to work with OpenTelemetry Ktor plugins.
2829

2930
## Server
3031

opentelemetry/README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# OpenTelemetry-Ktor Demo
2+
3+
[OpenTelemetry](https://opentelemetry.io/) provides support for Ktor with the `KtorClientTracing`and `KtorServerTracing`
4+
plugins for the Ktor client and server respectively. For the source code, see
5+
the [repository on GitHub](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/ktor).
6+
7+
This project contains extension functions for plugins that allow you to write code in the Ktor DSL style.
8+
9+
Take the following code as an example:
10+
11+
```kotlin
12+
install(KtorServerTracing) {
13+
...
14+
addAttributeExtractor(
15+
object : AttributesExtractor<ApplicationRequest, ApplicationResponse> {
16+
override fun onEnd(
17+
attributes: AttributesBuilder,
18+
context: Context,
19+
request: ApplicationRequest,
20+
response: ApplicationResponse?,
21+
error: Throwable?
22+
) {
23+
attributes.put("end-time", Instant.now().toEpochMilli())
24+
}
25+
}
26+
)
27+
...
28+
}
29+
```
30+
31+
Rewritten in Ktor DSL style, it looks like the following:
32+
33+
```kotlin
34+
install(KtorServerTracing) {
35+
...
36+
attributeExtractor {
37+
onEnd {
38+
attributes.put("end-time", Instant.now().toEpochMilli())
39+
}
40+
}
41+
...
42+
}
43+
```
44+
45+
You can find all extensions for the client plugin `KtorClientTracing` in
46+
the [extractions](./client/src/main/kotlin/opentelemetry/ktor/example/plugins/opentelemetry/extractions/) folder. \
47+
And you can find all extensions for the server plugin `KtorServerTracing` in
48+
the [extractions](./server/src/main/kotlin/opentelemetry/ktor/example/plugins/opentelemetry/extractions/) folder.
49+
50+
## Running
51+
52+
**Note:** You need to have [Docker](https://www.docker.com/) installed and running to run the sample.
53+
54+
To run this sample, execute the following command from the `opentelemetry` directory::
55+
56+
```bash
57+
./gradlew :runWithDocker
58+
```
59+
60+
It will start a `Jaeger` in the docker container (`Jaeger UI` available on http://localhost:16686/search) and
61+
then it will start a `server` on http://localhost:8080/
62+
63+
Then, to run the client, which will send requests to a server, you can execute the following command in
64+
an `opentelemetry` directory:
65+
66+
```bash
67+
./gradlew :client:run
68+
```
69+
70+
**Note:** In this example, we use
71+
an [Autoconfiguration OpenTelemetry instance](https://opentelemetry.io/docs/languages/java/instrumentation/#automatic-configuration),
72+
we set environment variables `OTEL_METRICS_EXPORTER` and `OTEL_EXPORTER_OTLP_ENDPOINT`
73+
in [build.gradle.kts](./build.gradle.kts) file.
74+
You can find more information about these environment variables
75+
in the [OpenTelemetry documentation](https://opentelemetry.io/docs/languages/sdk-configuration/).
76+
77+
Let's check what we will see in the `Jaeger UI` after running the server (with Docker) and the client:
78+
79+
1. We can see two our services that send opentelemetry data: `opentelemetry-ktor-sample-server`
80+
and `opentelemetry-ktor-sample-client`, and service `jaeger-all-in-one`, it's `Jaeger` tracing some of
81+
its components:
82+
![img.png](images/1.png)
83+
2. If you select `opentelemetry-ktor-sample-server` service and click on **Find traces**, you will see a list of traces:
84+
![img.png](images/2.png)
85+
3. If you click on one of those traces, you will be navigated to a screen providing detailed information about the
86+
selected trace:
87+
![img.png](images/3.png)

opentelemetry/build.gradle.kts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
description = "OpenTelemetry-Ktor example"
2+
3+
plugins {
4+
id("com.avast.gradle.docker-compose") version "0.14.0"
5+
}
6+
7+
subprojects {
8+
group = "opentelemetry.ktor.example"
9+
version = "0.0.1"
10+
11+
repositories {
12+
mavenCentral()
13+
}
14+
}
15+
16+
dockerCompose {
17+
useComposeFiles.add("docker/docker-compose.yml")
18+
}
19+
20+
tasks.register("runWithDocker") {
21+
dependsOn("composeUp", ":server:run")
22+
}
23+
24+
project(":server").setEnvironmentVariablesForOpenTelemetry()
25+
project(":client").setEnvironmentVariablesForOpenTelemetry()
26+
27+
fun Project.setEnvironmentVariablesForOpenTelemetry() {
28+
tasks.withType<JavaExec> {
29+
environment("OTEL_METRICS_EXPORTER", "none")
30+
environment("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317/")
31+
}
32+
}

opentelemetry/client/build.gradle.kts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
val ktor_version: String by project
2+
val logback_version: String by project
3+
val kotlin_version: String by project
4+
val opentelemetry_version: String by project
5+
6+
plugins {
7+
kotlin("jvm") version "1.9.21"
8+
id("io.ktor.plugin") version "2.3.8"
9+
id("application")
10+
}
11+
12+
application {
13+
mainClass.set("opentelemetry.ktor.example.ClientKt")
14+
15+
val isDevelopment: Boolean = project.ext.has("development")
16+
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
17+
}
18+
19+
dependencies {
20+
implementation(project(":shared"))
21+
22+
implementation("io.ktor:ktor-client-core-jvm")
23+
implementation("io.ktor:ktor-client-cio-jvm")
24+
implementation("io.ktor:ktor-client-websockets:$ktor_version")
25+
implementation("ch.qos.logback:logback-classic:$logback_version")
26+
27+
implementation("io.opentelemetry.instrumentation:opentelemetry-ktor-2.0:$opentelemetry_version-alpha")
28+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package opentelemetry.ktor.example
2+
3+
import io.ktor.client.*
4+
import io.ktor.client.engine.cio.*
5+
import io.ktor.client.plugins.*
6+
import io.ktor.client.plugins.websocket.*
7+
import opentelemetry.ktor.example.plugins.opentelemetry.setupClientTelemetry
8+
9+
suspend fun main() {
10+
val client = HttpClient(CIO) {
11+
install(WebSockets)
12+
13+
defaultRequest {
14+
url("http://$SERVER_HOST:$SERVER_PORT")
15+
}
16+
17+
setupClientTelemetry()
18+
}
19+
20+
doRequests(client)
21+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package opentelemetry.ktor.example
2+
3+
import io.ktor.client.*
4+
import io.ktor.client.plugins.websocket.*
5+
import io.ktor.client.request.*
6+
import io.ktor.websocket.*
7+
8+
9+
suspend fun doRequests(client: HttpClient) {
10+
// For this request you can see `CUSTOM` method instead of default `HTTP` in the Jaeger UI
11+
client.request("/known-methods") {
12+
method = CUSTOM_METHOD
13+
}
14+
15+
// For this request you can't see `CUSTOM_NOT_KNOWN` method, you can see default `HTTP` in the Jaeger UI
16+
client.request("/known-methods") {
17+
method = CUSTOM_METHOD_NOT_KNOWN
18+
}
19+
20+
// You can see tags `http.request.header.accept` and `http.response.header.content_type` for all requests
21+
// in the Jaeger UI and also `http.response.header.custom_header` for this request
22+
client.get("/captured-headers")
23+
24+
// For this request you can see tag `error=true` and `Error` icon only for server trace in the Jaeger UI
25+
client.get("/span-status-extractor")
26+
27+
// For this request you can see tag `span.kind=producer` only for server trace in the Jaeger UI
28+
client.post("/span-kind-extractor")
29+
30+
// You can see attribute `start-time` and `end-time` in the Jaeger UI for all requests
31+
client.get("/attribute-extractor")
32+
33+
// For this request you can see several spans and events only for server trace in the Jaeger UI
34+
client.get("/opentelemetry/tracer")
35+
36+
// For this request you can see several events only for server trace in the Jaeger UI
37+
client.ws("/opentelemetry/websocket") {
38+
send(Frame.Text("Hello, world!"))
39+
repeat(10) {
40+
send(incoming.receive())
41+
}
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package opentelemetry.ktor.example.plugins.opentelemetry.extractions
2+
3+
import io.ktor.client.request.*
4+
import io.ktor.client.statement.*
5+
import io.opentelemetry.api.common.AttributesBuilder
6+
import io.opentelemetry.context.Context
7+
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor
8+
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder
9+
10+
// addAttributeExtractor
11+
fun KtorClientTracingBuilder.attributeExtractor(
12+
extractorBuilder: ExtractorBuilder.() -> Unit = {}
13+
) {
14+
val builder = ExtractorBuilder().apply(extractorBuilder).build()
15+
addAttributesExtractors(
16+
object : AttributesExtractor<HttpRequestData, HttpResponse> {
17+
override fun onStart(
18+
attributes: AttributesBuilder,
19+
parentContext: Context,
20+
request: HttpRequestData
21+
) {
22+
builder.onStart(OnStartData(attributes, parentContext, request))
23+
}
24+
25+
override fun onEnd(
26+
attributes: AttributesBuilder,
27+
context: Context,
28+
request: HttpRequestData,
29+
response: HttpResponse?,
30+
error: Throwable?
31+
) {
32+
builder.onEnd(OnEndData(attributes, context, request, response, error))
33+
}
34+
}
35+
)
36+
}
37+
38+
class ExtractorBuilder {
39+
private var onStart: OnStartData.() -> Unit = {}
40+
private var onEnd: OnEndData.() -> Unit = {}
41+
42+
fun onStart(block: OnStartData.() -> Unit) {
43+
onStart = block
44+
}
45+
46+
fun onEnd(block: OnEndData.() -> Unit) {
47+
onEnd = block
48+
}
49+
50+
internal fun build(): Extractor {
51+
return Extractor(onStart, onEnd)
52+
}
53+
}
54+
55+
internal class Extractor(val onStart: OnStartData.() -> Unit, val onEnd: OnEndData.() -> Unit)
56+
57+
data class OnStartData(
58+
val attributes: AttributesBuilder,
59+
val parentContext: Context,
60+
val request: HttpRequestData
61+
)
62+
63+
data class OnEndData(
64+
val attributes: AttributesBuilder,
65+
val parentContext: Context,
66+
val request: HttpRequestData,
67+
val response: HttpResponse?,
68+
val error: Throwable?
69+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package opentelemetry.ktor.example.plugins.opentelemetry.extractions
2+
3+
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder
4+
5+
// setCapturedRequestHeaders
6+
fun KtorClientTracingBuilder.capturedRequestHeaders(vararg headers: String) {
7+
capturedRequestHeaders(headers.asIterable())
8+
}
9+
10+
fun KtorClientTracingBuilder.capturedRequestHeaders(headers: Iterable<String>) {
11+
setCapturedRequestHeaders(headers.toList())
12+
}
13+
14+
// setCapturedResponseHeaders
15+
fun KtorClientTracingBuilder.capturedResponseHeaders(vararg headers: String) {
16+
capturedResponseHeaders(headers.asIterable())
17+
}
18+
19+
fun KtorClientTracingBuilder.capturedResponseHeaders(headers: Iterable<String>) {
20+
setCapturedResponseHeaders(headers.toList())
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package opentelemetry.ktor.example.plugins.opentelemetry.extractions
2+
3+
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder
4+
5+
// setEmitExperimentalHttpClientMetrics
6+
fun KtorClientTracingBuilder.emitExperimentalHttpClientMetrics() {
7+
setEmitExperimentalHttpClientMetrics(true)
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package opentelemetry.ktor.example.plugins.opentelemetry.extractions
2+
3+
import io.ktor.http.*
4+
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracingBuilder
5+
6+
// setKnownMethods
7+
fun KtorClientTracingBuilder.knownMethods(vararg methods: HttpMethod) {
8+
knownMethods(methods.asIterable())
9+
}
10+
11+
fun KtorClientTracingBuilder.knownMethods(methods: Iterable<HttpMethod>) {
12+
setKnownMethods(methods.map { it.value }.toSet())
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package opentelemetry.ktor.example.plugins.opentelemetry
2+
3+
import io.ktor.client.*
4+
import io.ktor.client.engine.cio.*
5+
import io.ktor.http.*
6+
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracing
7+
import opentelemetry.ktor.example.CUSTOM_HEADER
8+
import opentelemetry.ktor.example.CUSTOM_METHOD
9+
import opentelemetry.ktor.example.getOpenTelemetry
10+
import opentelemetry.ktor.example.plugins.opentelemetry.extractions.*
11+
12+
/**
13+
* Install OpenTelemetry on the client.
14+
* You can see usages of new extension functions for [KtorClientTracing].
15+
*/
16+
fun HttpClientConfig<CIOEngineConfig>.setupClientTelemetry() {
17+
val openTelemetry = getOpenTelemetry(serviceName = "opentelemetry-ktor-sample-client")
18+
install(KtorClientTracing) {
19+
setOpenTelemetry(openTelemetry)
20+
21+
emitExperimentalHttpClientMetrics()
22+
23+
knownMethods(HttpMethod.DefaultMethods + CUSTOM_METHOD)
24+
capturedRequestHeaders(HttpHeaders.Accept)
25+
capturedResponseHeaders(HttpHeaders.ContentType, CUSTOM_HEADER)
26+
27+
attributeExtractor {
28+
onStart {
29+
attributes.put("start-time", System.currentTimeMillis())
30+
}
31+
onEnd {
32+
attributes.put("end-time", System.currentTimeMillis())
33+
}
34+
}
35+
}
36+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# This file is used to start the Jaeger all-in-one container
2+
version: '3.7'
3+
services:
4+
jaeger:
5+
image: jaegertracing/all-in-one:latest
6+
ports:
7+
- "4317:4317" # OTLP gRPC receiver
8+
- "16686:16686" # Jaeger UI

0 commit comments

Comments
 (0)