Skip to content

Commit 2dd3492

Browse files
committed
Initial APM-only openai-java instrumentation with a unit test.
1 parent a031cb4 commit 2dd3492

File tree

7 files changed

+278
-0
lines changed

7 files changed

+278
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
apply from: "$rootDir/gradle/java.gradle"
2+
apply plugin: 'idea'
3+
4+
muzzle {
5+
pass {
6+
group = "com.openai"
7+
module = "openai-java"
8+
versions = "[1.0.0,)"
9+
//TODO assertInverse = true
10+
}
11+
}
12+
13+
addTestSuiteForDir('latestDepTest', 'test')
14+
15+
dependencies {
16+
compileOnly group: 'com.openai', name: 'openai-java', version: '1.0.0'
17+
18+
testImplementation group: 'com.openai', name: 'openai-java', version: '1.0.0'
19+
latestDepTestImplementation group: 'com.openai', name: 'openai-java', version: '+'
20+
21+
testImplementation project(':dd-java-agent:instrumentation:okhttp:okhttp-3.0')
22+
}
23+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package datadog.trace.instrumentation.openai_java;
2+
3+
import com.openai.models.completions.Completion;
4+
import com.openai.models.completions.CompletionCreateParams;
5+
import datadog.trace.agent.tooling.Instrumenter;
6+
import datadog.trace.bootstrap.instrumentation.api.AgentScope;
7+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
8+
import net.bytebuddy.asm.Advice;
9+
10+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
11+
import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan;
12+
import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan;
13+
import static datadog.trace.instrumentation.openai_java.OpenAiDecorator.DECORATE;
14+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
15+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
16+
17+
public class CompletionServiceInstrumentation implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
18+
@Override
19+
public String instrumentedType() {
20+
return "com.openai.services.blocking.CompletionServiceImpl";
21+
}
22+
23+
@Override
24+
public void methodAdvice(MethodTransformer transformer) {
25+
transformer.applyAdvice(
26+
isMethod()
27+
.and(named("create"))
28+
.and(takesArgument(0, named("com.openai.models.completions.CompletionCreateParams"))),
29+
getClass().getName() + "$CreateAdvice");
30+
}
31+
32+
public static class CreateAdvice {
33+
@Advice.OnMethodEnter(suppress = Throwable.class)
34+
public static AgentScope enter(@Advice.Argument(0) final CompletionCreateParams params) {
35+
AgentSpan span = startSpan(OpenAiDecorator.INSTRUMENTATION_NAME, OpenAiDecorator.SPAN_NAME);
36+
DECORATE.afterStart(span);
37+
DECORATE.decorate(span, params);
38+
return activateSpan(span);
39+
}
40+
41+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
42+
public static void exit(@Advice.Enter final AgentScope scope, @Advice.Return Completion result, @Advice.Thrown final Throwable err) {
43+
final AgentSpan span = scope.span();
44+
if (err != null) {
45+
DECORATE.onError(span, err);
46+
}
47+
if (result != null) {
48+
DECORATE.decorate(span, result);
49+
}
50+
DECORATE.beforeFinish(span);
51+
scope.close();
52+
span.finish();
53+
}
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package datadog.trace.instrumentation.openai_java;
2+
3+
import com.openai.models.completions.Completion;
4+
import com.openai.models.completions.CompletionChoice;
5+
import com.openai.models.completions.CompletionCreateParams;
6+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
7+
import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes;
8+
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
9+
import datadog.trace.bootstrap.instrumentation.decorator.ClientDecorator;
10+
11+
public class OpenAiDecorator extends ClientDecorator {
12+
public static final OpenAiDecorator DECORATE = new OpenAiDecorator();
13+
14+
public static final String INSTRUMENTATION_NAME = "openai-java";
15+
public static final CharSequence SPAN_NAME = UTF8BytesString.create("openai.request");
16+
17+
private static final CharSequence COMPLETIONS_CREATE = UTF8BytesString.create("completions.create");
18+
19+
@Override
20+
protected String service() {
21+
return null;
22+
}
23+
24+
@Override
25+
protected String[] instrumentationNames() {
26+
return new String[] {
27+
INSTRUMENTATION_NAME
28+
};
29+
}
30+
31+
@Override
32+
protected CharSequence spanType() {
33+
return InternalSpanTypes.HTTP_CLIENT;
34+
}
35+
36+
@Override
37+
protected CharSequence component() {
38+
return null;
39+
}
40+
41+
public void decorate(AgentSpan span, CompletionCreateParams params) {
42+
span.setResourceName(COMPLETIONS_CREATE);
43+
44+
if (params == null) {
45+
return;
46+
}
47+
48+
span.setTag("openai.request.model", params.model().toString());
49+
//TODO set other tags: ai.provider, openai.request.endpoint, request.prompt?
50+
//TODO max_tokens, temperature or these are LLMObs – non-APM?
51+
}
52+
53+
public void decorate(AgentSpan span, Completion completion) {
54+
for (CompletionChoice choice : completion.choices()) {
55+
span.setTag("openai.response.choices." + choice.index() + ".text", choice.text()); // TODO
56+
}
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package datadog.trace.instrumentation.openai_java;
2+
3+
import com.google.auto.service.AutoService;
4+
import datadog.trace.agent.tooling.Instrumenter;
5+
import datadog.trace.agent.tooling.InstrumenterModule;
6+
import java.util.Arrays;
7+
import java.util.List;
8+
9+
@AutoService(InstrumenterModule.class)
10+
public class OpenAiModule extends InstrumenterModule.Tracing {
11+
public OpenAiModule() {
12+
super("openai-java");
13+
}
14+
15+
@Override
16+
public String[] helperClassNames() {
17+
return new String[] {
18+
packageName + ".OpenAiDecorator",
19+
};
20+
}
21+
22+
@Override
23+
public List<Instrumenter> typeInstrumentations() {
24+
return Arrays.asList(
25+
new CompletionServiceInstrumentation()
26+
);
27+
}
28+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import static datadog.trace.agent.test.utils.TraceUtils.runnableUnderTrace
2+
3+
import com.openai.models.completions.Completion
4+
import com.openai.models.completions.CompletionCreateParams
5+
6+
class CompletionServiceTest extends OpenAiTest {
7+
8+
def "test"() {
9+
CompletionCreateParams createParams = CompletionCreateParams.builder()
10+
.model(CompletionCreateParams.Model.GPT_3_5_TURBO_INSTRUCT)
11+
.prompt("Tell me a story about building the best SDK!")
12+
.build()
13+
14+
Completion completion = runnableUnderTrace("parent") {
15+
openAiClient.completions().create(createParams)
16+
}
17+
18+
expect:
19+
assertTraces(1) {
20+
trace(3) {
21+
span(0) {
22+
operationName "parent"
23+
parent()
24+
errored false
25+
}
26+
span(1) {
27+
operationName "openai.request"
28+
resourceName "completions.create"
29+
childOf span(0)
30+
errored false
31+
spanType "http"
32+
}
33+
span(2) {
34+
operationName "okhttp.request"
35+
// resourceName "POST /v1/completions"
36+
childOf span(1)
37+
errored false
38+
spanType "http"
39+
}
40+
}
41+
}
42+
}
43+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import com.google.common.base.Strings
2+
import com.openai.client.OpenAIClient
3+
import com.openai.client.okhttp.OpenAIOkHttpClient
4+
import com.openai.credential.BearerTokenCredential
5+
import datadog.trace.agent.test.InstrumentationSpecification
6+
import datadog.trace.agent.test.server.http.TestHttpServer
7+
import spock.lang.AutoCleanup
8+
import spock.lang.Shared
9+
10+
11+
class OpenAiTest extends InstrumentationSpecification {
12+
13+
// will use real openai backend when provided
14+
static String openAiToken() {
15+
return null
16+
}
17+
18+
@AutoCleanup
19+
@Shared
20+
OpenAIClient openAiClient
21+
22+
@AutoCleanup
23+
@Shared
24+
def mockOpenAiBackend = TestHttpServer.httpServer {
25+
handlers {
26+
prefix("/completions") {
27+
redirect("/v1/completions")
28+
}
29+
prefix("/v1/completions") {
30+
response.status(200).send(
31+
"""
32+
{
33+
"id": "cmpl-CUJTd66qbuEe2vu9cSEUWoFKFKm6O",
34+
"object": "text_completion",
35+
"created": 1761340745,
36+
"model": "gpt-3.5-turbo-instruct:20230824-v2",
37+
"choices": [
38+
{
39+
"text": "\\n\\nOnce upon a time in a tech company called Innovix, a team of",
40+
"index": 0,
41+
"logprobs": null,
42+
"finish_reason": "length"
43+
}
44+
],
45+
"usage": {
46+
"prompt_tokens": 10,
47+
"completion_tokens": 16,
48+
"total_tokens": 26
49+
}
50+
}
51+
"""
52+
)
53+
}
54+
}
55+
}
56+
57+
def setup() {
58+
OpenAIOkHttpClient.Builder b = OpenAIOkHttpClient.builder()
59+
if (Strings.isNullOrEmpty(openAiToken())) {
60+
// mock backend
61+
b.baseUrl(mockOpenAiBackend.address.toURL().toString())
62+
b.credential(BearerTokenCredential.create(""))
63+
} else {
64+
// real openai backend
65+
b.credential(BearerTokenCredential.create(openAiToken()))
66+
}
67+
openAiClient = b.build()
68+
}
69+
}
70+

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ include(
479479
":dd-java-agent:instrumentation:okhttp:okhttp-2.2",
480480
":dd-java-agent:instrumentation:okhttp:okhttp-3.0",
481481
":dd-java-agent:instrumentation:ognl-appsec",
482+
":dd-java-agent:instrumentation:openai-java:openai-java-1.0",
482483
":dd-java-agent:instrumentation:opensearch",
483484
":dd-java-agent:instrumentation:opensearch:rest",
484485
":dd-java-agent:instrumentation:opensearch:transport",

0 commit comments

Comments
 (0)