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 extension functions and example for OpenTelemetry #187

Merged
merged 6 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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
63 changes: 63 additions & 0 deletions opentelemetry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# OpenTelemetry-Ktor Demo

## Running
To run a sample, first, execute the following command in an `opentelemetry` directory:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

```bash
./gradlew :runWithDocker
marychatte marked this conversation as resolved.
Show resolved Hide resolved
```
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
```

[OpenTelemetry](https://opentelemetry.io/) has support for `Ktor`, you can find source code [here](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/ktor).
It contains plugins for client and server: `KtorClientTracing` and `KtorServerTracing`.

## Motivation
This project contains extension functions for plugins that allow you to write code in the Ktor DSL style. \
For example, you can rewrite the next code:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: "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())
}
}
)
...
}
```
To a more readable for `Ktor` style:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: "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.

## Examples
Let's see what we will see in the `Jaeger UI` after running the server (with Docker) and client:
1. We can see two services that send opentelemetry data: `opentelemetry-ktor-sample-server` and `opentelemetry-ktor-sample-client`:
![img.png](images/1.png)
2. If we choose `opentelemetry-ktor-sample-server` service, we will see the next traces:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: "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. And if we choose one of the traces:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: "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"

Check warning

Code scanning / detekt

Checks whether files end with a line separator. Warning

The file /home/runner/work/ktor-samples/ktor-samples/opentelemetry/build.gradle.kts is not ending with a new line.

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")
marychatte marked this conversation as resolved.
Show resolved Hide resolved
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

Check warning

Code scanning / detekt

Variable names should follow the naming convention set in the projects configuration. Warning

Variable names should match the pattern: [a-z][A-Za-z0-9]*

Check warning

Code scanning / detekt

Checks whether files end with a line separator. Warning

The file /home/runner/work/ktor-samples/ktor-samples/opentelemetry/client/build.gradle.kts is not ending with a new line.
val logback_version: String by project

Check warning

Code scanning / detekt

Variable names should follow the naming convention set in the projects configuration. Warning

Variable names should match the pattern: [a-z][A-Za-z0-9]*
val kotlin_version: String by project

Check warning

Code scanning / detekt

Variable names should follow the naming convention set in the projects configuration. Warning

Variable names should match the pattern: [a-z][A-Za-z0-9]*
val opentelemetry_version: String by project

Check warning

Code scanning / detekt

Variable names should follow the naming convention set in the projects configuration. Warning

Variable names should match the pattern: [a-z][A-Za-z0-9]*

plugins {
kotlin("jvm") version "1.9.21"
id("io.ktor.plugin") version "2.3.6"
marychatte marked this conversation as resolved.
Show resolved Hide resolved
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,23 @@
package opentelemetry.ktor.example

import io.ktor.client.*

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors. Warning

io.ktor.client.* is a wildcard import. Replace it with fully qualified imports.
import io.ktor.client.engine.cio.*

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors. Warning

io.ktor.client.engine.cio.* is a wildcard import. Replace it with fully qualified imports.
import io.ktor.client.plugins.*

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors. Warning

io.ktor.client.plugins.* is a wildcard import. Replace it with fully qualified imports.
import io.ktor.client.plugins.websocket.*

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors. Warning

io.ktor.client.plugins.websocket.* is a wildcard import. Replace it with fully qualified imports.
import opentelemetry.ktor.example.plugins.opentelemetry.installOpenTelemetryOnClient

suspend fun main() {
val openTelemetry = getOpenTelemetry(serviceName = "opentelemetry-ktor-sample-client")
marychatte marked this conversation as resolved.
Show resolved Hide resolved

val client = HttpClient(CIO) {
install(WebSockets)

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

installOpenTelemetryOnClient(openTelemetry)
marychatte marked this conversation as resolved.
Show resolved Hide resolved
}

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

Check warning

Code scanning / detekt

Checks whether files end with a line separator. Warning

The file /home/runner/work/ktor-samples/ktor-samples/opentelemetry/client/src/main/kotlin/opentelemetry/ktor/example/Requests.kt is not ending with a new line.

import io.ktor.client.*

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors. Warning

io.ktor.client.* is a wildcard import. Replace it with fully qualified imports.
import io.ktor.client.plugins.websocket.*

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors. Warning

io.ktor.client.plugins.websocket.* is a wildcard import. Replace it with fully qualified imports.
import io.ktor.client.request.*

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors. Warning

io.ktor.client.request.* is a wildcard import. Replace it with fully qualified imports.
import io.ktor.websocket.*

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors. Warning

io.ktor.websocket.* is a wildcard import. Replace it with fully qualified imports.


suspend fun doRequests(client: HttpClient) {
client.request("/known-methods") {
marychatte marked this conversation as resolved.
Show resolved Hide resolved
method = CUSTOM_METHOD
}

client.request("/known-methods") {
method = CUSTOM_METHOD_NOT_KNOWN
}

client.get("/captured-headers")

client.get("/span-status-extractor")

client.post("/span-kind-extractor")

client.get("/attribute-extractor")

client.get("/opentelemetry/tracer")

client.ws("/opentelemetry/websocket") {
send(Frame.Text("Hello, world!"))
repeat(10) {

Check warning

Code scanning / detekt

Report magic numbers. Magic number is a numeric literal that is not defined as a constant and hence it's unclear what the purpose of this number is. It's better to declare such numbers as constants and give them a proper name. By default, -1, 0, 1, and 2 are not considered to be magic numbers. Warning

This expression contains a magic number. Consider defining it to a well named constant.
send(incoming.receive())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package opentelemetry.ktor.example.plugins.opentelemetry

Check warning

Code scanning / detekt

Checks whether files end with a line separator. Warning

The file /home/runner/work/ktor-samples/ktor-samples/opentelemetry/client/src/main/kotlin/opentelemetry/ktor/example/plugins/opentelemetry/configureOpenTelemetryClient.kt is not ending with a new line.

import opentelemetry.ktor.example.CUSTOM_HEADER
import opentelemetry.ktor.example.CUSTOM_METHOD
import io.ktor.client.*

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors. Warning

io.ktor.client.* is a wildcard import. Replace it with fully qualified imports.
import io.ktor.client.engine.cio.*

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors. Warning

io.ktor.client.engine.cio.* is a wildcard import. Replace it with fully qualified imports.
import io.ktor.http.*

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors. Warning

io.ktor.http.* is a wildcard import. Replace it with fully qualified imports.
import io.opentelemetry.api.OpenTelemetry
import io.opentelemetry.instrumentation.ktor.v2_0.client.KtorClientTracing
import opentelemetry.ktor.example.plugins.opentelemetry.extractions.*

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors. Warning

opentelemetry.ktor.example.plugins.opentelemetry.extractions.* is a wildcard import. Replace it with fully qualified imports.


/**
* Install OpenTelemetry on the client.
* You can see usages of new extension functions for [KtorClientTracing].
*/
fun HttpClientConfig<CIOEngineConfig>.installOpenTelemetryOnClient(openTelemetry: OpenTelemetry) {
install(KtorClientTracing) {
setOpenTelemetry(openTelemetry)

emitExperimentalHttpClientMetrics()

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

attributeExtractor {
onStart {
attributes.put("start-time", System.currentTimeMillis())
}
onEnd {
attributes.put("end-time", System.currentTimeMillis())
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package opentelemetry.ktor.example.plugins.opentelemetry.extractions

Check warning

Code scanning / detekt

Checks whether files end with a line separator. Warning

The file /home/runner/work/ktor-samples/ktor-samples/opentelemetry/client/src/main/kotlin/opentelemetry/ktor/example/plugins/opentelemetry/extractions/attributeExtractor.kt is not ending with a new line.

import io.ktor.client.request.*

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors. Warning

io.ktor.client.request.* is a wildcard import. Replace it with fully qualified imports.
import io.ktor.client.statement.*

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors. Warning

io.ktor.client.statement.* is a wildcard import. Replace it with fully qualified imports.
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

Check warning

Code scanning / detekt

Checks whether files end with a line separator. Warning

The file /home/runner/work/ktor-samples/ktor-samples/opentelemetry/client/src/main/kotlin/opentelemetry/ktor/example/plugins/opentelemetry/extractions/capturedHeaders.kt is not ending with a new line.

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

Check warning

Code scanning / detekt

Checks whether files end with a line separator. Warning

The file /home/runner/work/ktor-samples/ktor-samples/opentelemetry/client/src/main/kotlin/opentelemetry/ktor/example/plugins/opentelemetry/extractions/emitExperimentalHttpClientMetrics.kt is not ending with a new line.

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

Check warning

Code scanning / detekt

Checks whether files end with a line separator. Warning

The file /home/runner/work/ktor-samples/ktor-samples/opentelemetry/client/src/main/kotlin/opentelemetry/ktor/example/plugins/opentelemetry/extractions/knownMethods.kt is not ending with a new line.

import io.ktor.http.*

Check warning

Code scanning / detekt

Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors. Warning

io.ktor.http.* is a wildcard import. Replace it with fully qualified imports.
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())
}
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
7 changes: 7 additions & 0 deletions opentelemetry/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ktor_version=2.3.6
kotlin_version=1.9.21
logback_version=1.4.11
kotlin.code.style=official

opentelemetry_version=1.32.0
opentelemetry_semconv_version=1.21.0-alpha
Binary file added opentelemetry/gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
5 changes: 5 additions & 0 deletions opentelemetry/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading
Loading