Skip to content

Commit 3cbd01c

Browse files
committed
Add initial instrumentation of OpenAI client
1 parent 7f7287b commit 3cbd01c

File tree

32 files changed

+3394
-0
lines changed

32 files changed

+3394
-0
lines changed

buildscripts/checkstyle.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@
254254
<property name="arrayInitIndent" value="2"/>
255255
</module>
256256
-->
257+
<!-- handled by error prone
257258
<module name="AbbreviationAsWordInName">
258259
<property name="ignoreFinal" value="false"/>
259260
<property name="allowedAbbreviationLength" value="0"/>
@@ -262,6 +263,7 @@
262263
PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF, PATTERN_VARIABLE_DEF, RECORD_DEF,
263264
RECORD_COMPONENT_DEF"/>
264265
</module>
266+
-->
265267
<module name="OverloadMethodsDeclarationOrder"/>
266268
<!-- there are only a few violations of this, and they all appear to be for good reasons
267269
<module name="VariableDeclarationUsageDistance"/>

conventions/src/main/kotlin/otel.scala-conventions.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ plugins {
55
}
66

77
dependencies {
8+
compileOnly("org.scala-lang:scala-library:2.11.12")
89
testCompileOnly("org.scala-lang:scala-library:2.11.12")
910
}
1011

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
plugins {
2+
id("otel.javaagent-instrumentation")
3+
}
4+
5+
muzzle {
6+
pass {
7+
group.set("com.openai")
8+
module.set("openai-java")
9+
versions.set("[1.1.0,)")
10+
assertInverse.set(true)
11+
}
12+
}
13+
14+
dependencies {
15+
implementation(project(":instrumentation:openai:openai-java-1.1:library"))
16+
17+
testInstrumentation(project(":instrumentation:okhttp:okhttp-3.0:javaagent"))
18+
19+
library("com.openai:openai-java:1.1.0")
20+
21+
testImplementation(project(":instrumentation:openai:openai-java-1.1:testing"))
22+
23+
// needed for latest dep tests
24+
testCompileOnly("com.google.errorprone:error_prone_annotations")
25+
}
26+
27+
tasks {
28+
withType<Test>().configureEach {
29+
systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean)
30+
// TODO run tests both with and without genai message capture
31+
systemProperty("otel.instrumentation.genai.capture-message-content", "true")
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.openai.v1_1;
7+
8+
import static io.opentelemetry.javaagent.instrumentation.openai.v1_1.OpenAISingletons.TELEMETRY;
9+
import static net.bytebuddy.matcher.ElementMatchers.named;
10+
import static net.bytebuddy.matcher.ElementMatchers.returns;
11+
12+
import com.openai.client.OpenAIClient;
13+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
14+
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
15+
import net.bytebuddy.asm.Advice;
16+
import net.bytebuddy.description.type.TypeDescription;
17+
import net.bytebuddy.matcher.ElementMatcher;
18+
19+
@SuppressWarnings("IdentifierName") // Want to match library's convention
20+
public class OpenAIClientInstrumentation implements TypeInstrumentation {
21+
@Override
22+
public ElementMatcher<TypeDescription> typeMatcher() {
23+
return named("com.openai.client.okhttp.OpenAIOkHttpClient$Builder");
24+
}
25+
26+
@Override
27+
public void transform(TypeTransformer transformer) {
28+
transformer.applyAdviceToMethod(
29+
named("build").and(returns(named("com.openai.client.OpenAIClient"))),
30+
OpenAIClientInstrumentation.class.getName() + "$BuildAdvice");
31+
}
32+
33+
@SuppressWarnings("unused")
34+
public static class BuildAdvice {
35+
@Advice.OnMethodExit
36+
@Advice.AssignReturned.ToReturned
37+
public static OpenAIClient onExit(@Advice.Return OpenAIClient client) {
38+
return TELEMETRY.wrap(client);
39+
}
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.openai.v1_1;
7+
8+
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
9+
import static java.util.Collections.singletonList;
10+
11+
import com.google.auto.service.AutoService;
12+
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
13+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
14+
import java.util.List;
15+
import net.bytebuddy.matcher.ElementMatcher;
16+
17+
@AutoService(InstrumentationModule.class)
18+
@SuppressWarnings("IdentifierName") // Want to match library's convention
19+
public class OpenAIInstrumentationModule extends InstrumentationModule {
20+
public OpenAIInstrumentationModule() {
21+
super("openai", "openai-java", "openai-java-1.1");
22+
}
23+
24+
@Override
25+
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
26+
return hasClassesNamed("com.openai.client.OpenAIClient");
27+
}
28+
29+
@Override
30+
public List<TypeInstrumentation> typeInstrumentations() {
31+
return singletonList(new OpenAIClientInstrumentation());
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.openai.v1_1;
7+
8+
import io.opentelemetry.api.GlobalOpenTelemetry;
9+
import io.opentelemetry.instrumentation.openai.v1_1.OpenAITelemetry;
10+
import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig;
11+
12+
@SuppressWarnings("IdentifierName") // Want to match library's convention
13+
public final class OpenAISingletons {
14+
public static final OpenAITelemetry TELEMETRY =
15+
OpenAITelemetry.builder(GlobalOpenTelemetry.get())
16+
.setCaptureMessageContent(
17+
AgentInstrumentationConfig.get()
18+
.getBoolean("otel.instrumentation.genai.capture-message-content", false))
19+
.build();
20+
21+
private OpenAISingletons() {}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.openai.v1_1;
7+
8+
import com.openai.client.OpenAIClient;
9+
import io.opentelemetry.instrumentation.openai.v1_1.AbstractChatTest;
10+
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
11+
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
12+
import io.opentelemetry.sdk.testing.assertj.SpanDataAssert;
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
import java.util.function.Consumer;
16+
import org.junit.jupiter.api.extension.RegisterExtension;
17+
18+
class ChatTest extends AbstractChatTest {
19+
20+
@RegisterExtension
21+
private static final AgentInstrumentationExtension testing =
22+
AgentInstrumentationExtension.create();
23+
24+
@Override
25+
protected InstrumentationExtension getTesting() {
26+
return testing;
27+
}
28+
29+
@Override
30+
protected OpenAIClient wrap(OpenAIClient client) {
31+
return client;
32+
}
33+
34+
@Override
35+
protected final List<Consumer<SpanDataAssert>> maybeWithTransportSpan(
36+
Consumer<SpanDataAssert> span) {
37+
List<Consumer<SpanDataAssert>> result = new ArrayList<>();
38+
result.add(span);
39+
// Do a very simple assertion since the telemetry is not part of this library.
40+
result.add(s -> s.hasName("POST"));
41+
return result;
42+
}
43+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
plugins {
2+
id("otel.library-instrumentation")
3+
id("otel.nullaway-conventions")
4+
}
5+
6+
dependencies {
7+
library("com.openai:openai-java:1.1.0")
8+
9+
testImplementation(project(":instrumentation:openai:openai-java-1.1:testing"))
10+
11+
// needed for latest dep tests
12+
testCompileOnly("com.google.errorprone:error_prone_annotations")
13+
}
14+
15+
tasks {
16+
withType<Test>().configureEach {
17+
systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean)
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.openai.v1_1;
7+
8+
import static java.util.Collections.emptyList;
9+
import static java.util.Collections.singletonList;
10+
11+
import com.openai.models.chat.completions.ChatCompletion;
12+
import com.openai.models.chat.completions.ChatCompletionCreateParams;
13+
import com.openai.models.completions.CompletionUsage;
14+
import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesGetter;
15+
import java.util.List;
16+
import java.util.stream.Collectors;
17+
import org.jetbrains.annotations.Nullable;
18+
19+
enum ChatAttributesGetter
20+
implements GenAiAttributesGetter<ChatCompletionCreateParams, ChatCompletion> {
21+
INSTANCE;
22+
23+
@Override
24+
public String getOperationName(ChatCompletionCreateParams request) {
25+
return GenAiAttributes.GenAiOperationNameIncubatingValues.CHAT;
26+
}
27+
28+
@Override
29+
public String getSystem(ChatCompletionCreateParams request) {
30+
return GenAiAttributes.GenAiSystemIncubatingValues.OPENAI;
31+
}
32+
33+
@Override
34+
public String getRequestModel(ChatCompletionCreateParams request) {
35+
return request.model().asString();
36+
}
37+
38+
@Nullable
39+
@Override
40+
public Long getRequestSeed(ChatCompletionCreateParams request) {
41+
return request.seed().orElse(null);
42+
}
43+
44+
@Nullable
45+
@Override
46+
public List<String> getRequestEncodingFormats(ChatCompletionCreateParams request) {
47+
return null;
48+
}
49+
50+
@Nullable
51+
@Override
52+
public Double getRequestFrequencyPenalty(ChatCompletionCreateParams request) {
53+
return request.frequencyPenalty().orElse(null);
54+
}
55+
56+
@Nullable
57+
@Override
58+
public Long getRequestMaxTokens(ChatCompletionCreateParams request) {
59+
return request.maxCompletionTokens().orElse(null);
60+
}
61+
62+
@Nullable
63+
@Override
64+
public Double getRequestPresencePenalty(ChatCompletionCreateParams request) {
65+
return request.presencePenalty().orElse(null);
66+
}
67+
68+
@Nullable
69+
@Override
70+
public List<String> getRequestStopSequences(ChatCompletionCreateParams request) {
71+
return request
72+
.stop()
73+
.map(
74+
s -> {
75+
if (s.isString()) {
76+
return singletonList(s.asString());
77+
}
78+
if (s.isStrings()) {
79+
return s.asStrings();
80+
}
81+
return null;
82+
})
83+
.orElse(null);
84+
}
85+
86+
@Nullable
87+
@Override
88+
public Double getRequestTemperature(ChatCompletionCreateParams request) {
89+
return request.temperature().orElse(null);
90+
}
91+
92+
@Nullable
93+
@Override
94+
public Double getRequestTopK(ChatCompletionCreateParams request) {
95+
return null;
96+
}
97+
98+
@Nullable
99+
@Override
100+
public Double getRequestTopP(ChatCompletionCreateParams request) {
101+
return request.topP().orElse(null);
102+
}
103+
104+
@Override
105+
public List<String> getResponseFinishReasons(
106+
ChatCompletionCreateParams request, @Nullable ChatCompletion response) {
107+
if (response == null) {
108+
return emptyList();
109+
}
110+
return response.choices().stream()
111+
.map(choice -> choice.finishReason().asString())
112+
.collect(Collectors.toList());
113+
}
114+
115+
@Override
116+
@Nullable
117+
public String getResponseId(
118+
ChatCompletionCreateParams request, @Nullable ChatCompletion response) {
119+
if (response == null) {
120+
return null;
121+
}
122+
return response.id();
123+
}
124+
125+
@Override
126+
@Nullable
127+
public String getResponseModel(
128+
ChatCompletionCreateParams request, @Nullable ChatCompletion response) {
129+
if (response == null) {
130+
return null;
131+
}
132+
return response.model();
133+
}
134+
135+
@Override
136+
@Nullable
137+
public Long getUsageInputTokens(
138+
ChatCompletionCreateParams request, @Nullable ChatCompletion response) {
139+
if (response == null) {
140+
return null;
141+
}
142+
return response.usage().map(CompletionUsage::promptTokens).orElse(null);
143+
}
144+
145+
@Override
146+
@Nullable
147+
public Long getUsageOutputTokens(
148+
ChatCompletionCreateParams request, @Nullable ChatCompletion response) {
149+
if (response == null) {
150+
return null;
151+
}
152+
return response.usage().map(CompletionUsage::completionTokens).orElse(null);
153+
}
154+
}

0 commit comments

Comments
 (0)