-
Notifications
You must be signed in to change notification settings - Fork 345
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
Changes from 1 commit
fea8595
73237fe
110d849
98b7ffc
3a8e639
a1a127a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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: | ||
```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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Edit: "If you select |
||
![img.png](images/2.png) | ||
3. And if we choose one of the traces: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
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/") | ||
} | ||
} |
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()) | ||
} |
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 |
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 |
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 |
There was a problem hiding this comment.
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:"