Skip to content

Commit a304e08

Browse files
authored
Add experimental instrumentation for OpenAI client (#497)
1 parent 11b0e52 commit a304e08

40 files changed

+6971
-91
lines changed

CHANGELOG.next-release.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
* add dynamically disabled instrumentation capability - #422
22
* add disable all instrumentations option - #471
33
* add stop-sending option - #474
4+
* Add OpenAI client instrumentation - #497

agentextension/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ plugins {
22
id("elastic-otel.java-conventions")
33
id("elastic-otel.sign-and-publish-conventions")
44
id("elastic-otel.license-report-conventions")
5-
id("com.github.johnrengelman.shadow")
5+
id("com.gradleup.shadow")
66
}
77

88
description = "Bundles all elastic extensions in a fat-jar to be used" +

buildSrc/build.gradle.kts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,15 @@ repositories {
1515

1616
dependencies {
1717
implementation(catalog.spotlessPlugin)
18-
implementation(catalog.shadowPlugin)
1918
implementation(catalog.licenseReportPlugin)
19+
// muzzle pulls in ancient versions of http components which conflict with other plugins, such as jib
20+
implementation(catalog.muzzleGenerationPlugin) {
21+
exclude(group = "org.apache.httpcomponents" )
22+
}
23+
implementation(catalog.muzzleCheckPlugin) {
24+
exclude(group = "org.apache.httpcomponents" )
25+
}
26+
implementation(catalog.shadowPlugin)
2027
// The ant dependency is required to add custom transformers for the shadow plugin
2128
// but it is unfortunately not exposed as transitive dependency
2229
implementation(catalog.ant)

buildSrc/src/main/kotlin/elastic-otel.agent-packaging-conventions.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import org.objectweb.asm.Opcodes
88

99
plugins {
1010
id("elastic-otel.java-conventions")
11-
id("com.github.johnrengelman.shadow")
11+
id("com.gradleup.shadow")
1212
}
1313

1414

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
2+
3+
plugins {
4+
`java-library`
5+
id("elastic-otel.java-conventions")
6+
id("io.opentelemetry.instrumentation.muzzle-generation")
7+
id("io.opentelemetry.instrumentation.muzzle-check")
8+
}
9+
10+
// Other instrumentations to include for testing
11+
val testInstrumentation: Configuration by configurations.creating {
12+
isCanBeConsumed = false
13+
}
14+
val agentForTesting: Configuration by configurations.creating {
15+
isCanBeConsumed = false
16+
}
17+
18+
//https://github.com/gradle/gradle/issues/15383
19+
val catalog = extensions.getByType<VersionCatalogsExtension>().named("catalog")
20+
dependencies {
21+
agentForTesting(platform(catalog.findLibrary("opentelemetryInstrumentationAlphaBom").get()))
22+
agentForTesting("io.opentelemetry.javaagent:opentelemetry-agent-for-testing")
23+
24+
compileOnly("io.opentelemetry:opentelemetry-sdk")
25+
compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api")
26+
compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api")
27+
28+
testImplementation("io.opentelemetry.javaagent:opentelemetry-testing-common")
29+
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
30+
31+
val agentVersion = catalog.findVersion("opentelemetryJavaagentAlpha").get()
32+
add("codegen", "io.opentelemetry.javaagent:opentelemetry-javaagent-tooling:${agentVersion}")
33+
add("muzzleBootstrap", "io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations-support:${agentVersion}")
34+
add("muzzleTooling", "io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api:${agentVersion}")
35+
add("muzzleTooling", "io.opentelemetry.javaagent:opentelemetry-javaagent-tooling:${agentVersion}")
36+
}
37+
38+
fun relocatePackages( shadowJar : ShadowJar) {
39+
// rewrite dependencies calling Logger.getLogger
40+
shadowJar.relocate("java.util.logging.Logger", "io.opentelemetry.javaagent.bootstrap.PatchLogger")
41+
42+
// prevents conflict with library instrumentation, since these classes live in the bootstrap class loader
43+
shadowJar.relocate("io.opentelemetry.instrumentation", "io.opentelemetry.javaagent.shaded.instrumentation") {
44+
// Exclude resource providers since they live in the agent class loader
45+
exclude("io.opentelemetry.instrumentation.resources.*")
46+
exclude("io.opentelemetry.instrumentation.spring.resources.*")
47+
}
48+
49+
// relocate(OpenTelemetry API) since these classes live in the bootstrap class loader
50+
shadowJar.relocate("io.opentelemetry.api", "io.opentelemetry.javaagent.shaded.io.opentelemetry.api")
51+
shadowJar.relocate("io.opentelemetry.semconv", "io.opentelemetry.javaagent.shaded.io.opentelemetry.semconv")
52+
shadowJar.relocate("io.opentelemetry.context", "io.opentelemetry.javaagent.shaded.io.opentelemetry.context")
53+
shadowJar.relocate("io.opentelemetry.extension.incubator", "io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.incubator")
54+
55+
// relocate the OpenTelemetry extensions that are used by instrumentation modules
56+
// these extensions live in the AgentClassLoader, and are injected into the user's class loader
57+
// by the instrumentation modules that use them
58+
shadowJar.relocate("io.opentelemetry.extension.aws", "io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.aws")
59+
shadowJar.relocate("io.opentelemetry.extension.kotlin", "io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.kotlin")
60+
}
61+
62+
tasks {
63+
shadowJar {
64+
configurations = listOf(project.configurations.runtimeClasspath.get(), testInstrumentation)
65+
mergeServiceFiles()
66+
67+
archiveFileName.set("agent-testing.jar")
68+
relocatePackages(this)
69+
}
70+
}
71+
72+
tasks.withType<Test>().configureEach {
73+
dependsOn(tasks.shadowJar, agentForTesting)
74+
75+
jvmArgs(
76+
"-Dotel.javaagent.debug=true",
77+
"-javaagent:${agentForTesting.files.first().absolutePath}",
78+
// loads the given just jar, but in contrast to external extensions doesn't perform runtime shading
79+
// instead the instrumentations are expected to be correctly shaded already in the jar
80+
// Also the classes end up in the agent classloader instead of the extension loader
81+
"-Dotel.javaagent.experimental.initializer.jar=${tasks.shadowJar.get().archiveFile.get().asFile.absolutePath}",
82+
"-Dotel.javaagent.testing.additional-library-ignores.enabled=false",
83+
"-Dotel.javaagent.testing.fail-on-context-leak=true",
84+
"-Dotel.javaagent.testing.transform-safe-logging.enabled=true",
85+
"-Dotel.metrics.exporter=otlp"
86+
)
87+
88+
// The sources are packaged into the testing jar so we need to make sure to exclude from the test
89+
// classpath, which automatically inherits them, to ensure our shaded versions are used.
90+
classpath = classpath.filter {
91+
return@filter !(it == file("${layout.buildDirectory.get()}/resources/main") || it == file("${layout.buildDirectory.get()}/classes/java/main"))
92+
}
93+
}

custom/build.gradle.kts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@ plugins {
22
id("elastic-otel.library-packaging-conventions")
33
}
44

5+
val instrumentations = listOf<String>(
6+
":instrumentation:openai-client-instrumentation"
7+
)
8+
59
dependencies {
610
implementation(project(":common"))
711
implementation(project(":inferred-spans"))
812
implementation(project(":universal-profiling-integration"))
913
implementation(project(":resources"))
14+
instrumentations.forEach {
15+
implementation(project(it))
16+
}
17+
1018
compileOnly("io.opentelemetry:opentelemetry-sdk")
1119
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
1220
compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api")
@@ -37,6 +45,21 @@ dependencies {
3745
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
3846
testImplementation(libs.freemarker)
3947
}
48+
49+
tasks {
50+
instrumentations.forEach {
51+
// TODO: instrumentation dependencies must be declared here explicitly atm, otherwise gradle complains
52+
// about it being missing we need to figure out a way of including it in the "compileJava" task
53+
// to not have to do this
54+
javadoc {
55+
dependsOn("$it:byteBuddyJava")
56+
}
57+
compileTestJava {
58+
dependsOn("$it:byteBuddyJava")
59+
}
60+
}
61+
}
62+
4063
tasks.withType<Test> {
4164
val overrideConfig = project.properties["elastic.otel.overwrite.config.docs"]
4265
if (overrideConfig != null) {

docs/configure.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,26 @@ _Currently there are no additional `OTEL_` options waiting to be contributed ups
111111
| `ELASTIC_OTEL_UNIVERSAL_PROFILING_INTEGRATION_*` | [Universal profiling integration](https://github.com/elastic/elastic-otel-java/tree/main/universal-profiling-integration) | Correlates traces with profiling data from the Elastic universal profiler. |
112112
| `ELASTIC_OTEL_JAVA_SPAN_STACKTRACE_MIN_DURATION` | [Span stacktrace capture](https://github.com/open-telemetry/opentelemetry-java-contrib/tree/main/span-stacktrace) | Define the minimum duration for attaching stack traces to spans. Defaults to 5ms. |
113113

114+
### Instrumentations that are _only_ available in EDOT Java
115+
116+
Some instrumentation are only available in EDOT Java and might or might not be added upstream in future versions.
117+
118+
#### OpenAI Java Client
119+
120+
Instrumentation for the [official OpenAI Java Client](https://github.com/openai/openai-java).
121+
It supports:
122+
* Tracing for requests, including GenAI-specific attributes such as token usage
123+
* Opt-In logging of OpenAI request and response content payloads
124+
125+
This instrumentation is currently **experimental**. It can by disabled by setting either the `OTEL_INSTRUMENTATION_OPENAI_CLIENT_ENABLED` environment variable or the `otel.instrumentation.openai-client.enabled` JVM property to `false`.
126+
127+
In addition, this instrumentation provides the following configuration options:
128+
129+
| Option(s) | Description |
130+
|-------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
131+
| `ELASTIC_OTEL_JAVA_INSTRUMENTATION_GENAI_EMIT_EVENTS` | If set to `true`, the agent will generate log events for OpenAI requests and responses. Potentially sensitive content will only be included if `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` is true. Defaults to the value of `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`, so that just enabling `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` ensures that log events are generated. |
132+
| `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | If set to `true`, enables the capturing of OpenAI request and response content in the log events outputted by the agent. Defaults to `false` |
133+
114134
<!-- ✅ List auth methods -->
115135
## Authentication methods
116136

gradle/instrumentation.gradle

Lines changed: 0 additions & 68 deletions
This file was deleted.

gradle/libs.versions.toml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[versions]
2-
shadow = "8.1.1"
2+
shadow = "8.3.5"
33
jib = "3.4.4"
44
spotless = "7.0.1"
55
junit = "5.11.4"
@@ -22,6 +22,9 @@ opentelemetryContribAlpha = "1.42.0-alpha"
2222
# reference the "incubating" version explicitly
2323
opentelemetrySemconvAlpha = "1.29.0-alpha"
2424

25+
# instrumented libraries
26+
openaiClient = "0.11.0"
27+
2528
[libraries]
2629

2730
# transitively provides 'opentelemetry-instrumentation-bom' (non-alpha)
@@ -44,6 +47,7 @@ autoservice-annotations = { group = "com.google.auto.service", name = "auto-serv
4447
assertj-core = "org.assertj:assertj-core:3.27.2"
4548
awaitility = "org.awaitility:awaitility:4.2.2"
4649
findbugs-jsr305 = "com.google.code.findbugs:jsr305:3.0.2"
50+
wiremock = "com.github.tomakehurst:wiremock-jre8:2.35.2"
4751
testcontainers = "org.testcontainers:testcontainers:1.20.4"
4852
logback = "ch.qos.logback:logback-classic:1.5.16"
4953
jackson = "com.fasterxml.jackson.core:jackson-databind:2.18.2"
@@ -66,13 +70,18 @@ asyncprofiler = "tools.profiler:async-profiler:3.0"
6670
freemarker = "org.freemarker:freemarker:2.3.34"
6771

6872
spotlessPlugin = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotless" }
69-
shadowPlugin = { group = "com.github.johnrengelman", name = "shadow", version.ref = "shadow" }
73+
shadowPlugin = { group = "com.gradleup.shadow", name = "shadow-gradle-plugin", version.ref = "shadow" }
7074
licenseReportPlugin = "com.github.jk1.dependency-license-report:com.github.jk1.dependency-license-report.gradle.plugin:2.9"
75+
muzzleCheckPlugin = { group = "io.opentelemetry.instrumentation.muzzle-check", name = "io.opentelemetry.instrumentation.muzzle-check.gradle.plugin", version.ref = "opentelemetryJavaagentAlpha" }
76+
muzzleGenerationPlugin = { group = "io.opentelemetry.instrumentation.muzzle-generation", name = "io.opentelemetry.instrumentation.muzzle-generation.gradle.plugin", version.ref = "opentelemetryJavaagentAlpha" }
7177
# Ant should be kept in sync with the version used in the shadow plugin
7278
ant = "org.apache.ant:ant:1.10.15"
7379
# ASM is currently only used during compile-time, so it is okay to diverge from the version used in ByteBuddy
7480
asm = "org.ow2.asm:asm:9.7"
7581

82+
# Instrumented libraries
83+
openaiClient = {group = "com.openai", name = "openai-java", version.ref ="openaiClient"}
84+
7685
[bundles]
7786

7887
semconv = ["opentelemetrySemconv", "opentelemetrySemconvIncubating"]
@@ -84,4 +93,3 @@ taskinfo = { id = "org.barfuin.gradle.taskinfo", version = '2.2.0' }
8493
jmh = {id = "me.champeau.jmh", version = "0.7.2"}
8594
nexusPublish = { id = "io.github.gradle-nexus.publish-plugin", version = '2.0.0' }
8695
dockerJavaApplication = { id = "com.bmuschko.docker-java-application", version = "9.4.0" }
87-
shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" }

instrumentation/build.gradle

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)