Skip to content

Commit 1b58fef

Browse files
authored
Merge pull request #24 from braintrustdata/ark/langchain4j-instrumentation
Instrumentation for Lanchain4j's OpenAI Chat Model
2 parents 1db1ece + f09d73b commit 1b58fef

File tree

8 files changed

+912
-1
lines changed

8 files changed

+912
-1
lines changed

build.gradle

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ ext {
4646
jacksonVersion = '2.16.1'
4747
junitVersion = '5.11.4'
4848
slf4jVersion = '2.0.17'
49+
langchainVersion = '1.9.1'
4950
}
5051

5152
dependencies {
@@ -86,6 +87,14 @@ dependencies {
8687
// Google GenAI Instrumentation
8788
compileOnly "com.google.genai:google-genai:1.20.0"
8889
testImplementation "com.google.genai:google-genai:1.20.0"
90+
91+
// LangChain4j Instrumentation
92+
compileOnly "dev.langchain4j:langchain4j:${langchainVersion}"
93+
compileOnly "dev.langchain4j:langchain4j-http-client:${langchainVersion}"
94+
compileOnly "dev.langchain4j:langchain4j-open-ai:${langchainVersion}"
95+
testImplementation "dev.langchain4j:langchain4j:${langchainVersion}"
96+
testImplementation "dev.langchain4j:langchain4j-http-client:${langchainVersion}"
97+
testImplementation "dev.langchain4j:langchain4j-open-ai:${langchainVersion}"
8998
}
9099

91100
/**

examples/build.gradle

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ dependencies {
3535
implementation('org.springframework.boot:spring-boot-starter:3.4.1') {
3636
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
3737
}
38+
// to run langchain4j examples
39+
implementation 'dev.langchain4j:langchain4j:1.9.1'
40+
implementation 'dev.langchain4j:langchain4j-open-ai:1.9.1'
3841
}
3942

4043
application {
@@ -142,7 +145,6 @@ task runSpringAI(type: JavaExec) {
142145
}
143146
}
144147

145-
146148
task runRemoteEval(type: JavaExec) {
147149
group = 'Braintrust SDK Examples'
148150
description = 'Run the remote eval example'
@@ -156,3 +158,17 @@ task runRemoteEval(type: JavaExec) {
156158
suspend = false
157159
}
158160
}
161+
162+
task runLangchain(type: JavaExec) {
163+
group = 'Braintrust SDK Examples'
164+
description = 'Run the LangChain4j instrumentation example. NOTE: this requires OPENAI_API_KEY to be exported and will make a small call to openai, using your tokens'
165+
classpath = sourceSets.main.runtimeClasspath
166+
mainClass = 'dev.braintrust.examples.LangchainExample'
167+
systemProperty 'org.slf4j.simpleLogger.log.dev.braintrust', braintrustLogLevel
168+
debugOptions {
169+
enabled = true
170+
port = 5566
171+
server = true
172+
suspend = false
173+
}
174+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package dev.braintrust.examples;
2+
3+
import dev.braintrust.Braintrust;
4+
import dev.braintrust.instrumentation.langchain.BraintrustLangchain;
5+
import dev.langchain4j.data.message.UserMessage;
6+
import dev.langchain4j.model.chat.ChatModel;
7+
import dev.langchain4j.model.openai.OpenAiChatModel;
8+
9+
/** Basic OTel + LangChain4j instrumentation example */
10+
public class LangchainExample {
11+
12+
public static void main(String[] args) throws Exception {
13+
if (null == System.getenv("OPENAI_API_KEY")) {
14+
System.err.println(
15+
"\nWARNING envar OPENAI_API_KEY not found. This example will likely fail.\n");
16+
}
17+
var braintrust = Braintrust.get();
18+
var openTelemetry = braintrust.openTelemetryCreate();
19+
20+
ChatModel model =
21+
BraintrustLangchain.wrap(
22+
openTelemetry,
23+
OpenAiChatModel.builder()
24+
.apiKey(System.getenv("OPENAI_API_KEY"))
25+
.modelName("gpt-4o-mini")
26+
.temperature(0.0));
27+
28+
var rootSpan =
29+
openTelemetry
30+
.getTracer("my-instrumentation")
31+
.spanBuilder("langchain4j-instrumentation-example")
32+
.startSpan();
33+
try (var ignored = rootSpan.makeCurrent()) {
34+
chatExample(model);
35+
} finally {
36+
rootSpan.end();
37+
}
38+
var url =
39+
braintrust.projectUri()
40+
+ "/logs?r=%s&s=%s"
41+
.formatted(
42+
rootSpan.getSpanContext().getTraceId(),
43+
rootSpan.getSpanContext().getSpanId());
44+
System.out.println(
45+
"\n\n Example complete! View your data in Braintrust: %s\n".formatted(url));
46+
}
47+
48+
private static void chatExample(ChatModel model) {
49+
var message = UserMessage.from("What is the capital of France?");
50+
var response = model.chat(message);
51+
System.out.println(
52+
"\n~~~ LANGCHAIN4J CHAT RESPONSE: %s\n".formatted(response.aiMessage().text()));
53+
}
54+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package dev.braintrust.instrumentation.langchain;
2+
3+
import dev.langchain4j.http.client.HttpClientBuilder;
4+
import dev.langchain4j.http.client.HttpClientBuilderLoader;
5+
import dev.langchain4j.model.openai.OpenAiChatModel;
6+
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
7+
import io.opentelemetry.api.OpenTelemetry;
8+
import lombok.extern.slf4j.Slf4j;
9+
10+
/** Braintrust LangChain4j client instrumentation. */
11+
@Slf4j
12+
public final class BraintrustLangchain {
13+
/** Instrument langchain openai chat model with braintrust traces */
14+
public static OpenAiChatModel wrap(
15+
OpenTelemetry otel, OpenAiChatModel.OpenAiChatModelBuilder builder) {
16+
try {
17+
HttpClientBuilder underlyingHttpClient = getPrivateField(builder, "httpClientBuilder");
18+
if (underlyingHttpClient == null) {
19+
underlyingHttpClient = HttpClientBuilderLoader.loadHttpClientBuilder();
20+
}
21+
HttpClientBuilder wrappedHttpClient =
22+
wrap(otel, underlyingHttpClient, new Options("openai"));
23+
return builder.httpClientBuilder(wrappedHttpClient).build();
24+
} catch (Exception e) {
25+
log.warn(
26+
"Braintrust instrumentation could not be applied to OpenAiChatModel builder",
27+
e);
28+
return builder.build();
29+
}
30+
}
31+
32+
/** Instrument langchain openai chat model with braintrust traces */
33+
public static OpenAiStreamingChatModel wrap(
34+
OpenTelemetry otel, OpenAiStreamingChatModel.OpenAiStreamingChatModelBuilder builder) {
35+
try {
36+
HttpClientBuilder underlyingHttpClient = getPrivateField(builder, "httpClientBuilder");
37+
if (underlyingHttpClient == null) {
38+
underlyingHttpClient = HttpClientBuilderLoader.loadHttpClientBuilder();
39+
}
40+
HttpClientBuilder wrappedHttpClient =
41+
wrap(otel, underlyingHttpClient, new Options("openai"));
42+
return builder.httpClientBuilder(wrappedHttpClient).build();
43+
} catch (Exception e) {
44+
log.warn(
45+
"Braintrust instrumentation could not be applied to OpenAiStreamingChatModel"
46+
+ " builder",
47+
e);
48+
return builder.build();
49+
}
50+
}
51+
52+
private static HttpClientBuilder wrap(
53+
OpenTelemetry otel, HttpClientBuilder builder, Options options) {
54+
return new WrappedHttpClientBuilder(otel, builder, options);
55+
}
56+
57+
public record Options(String providerName) {}
58+
59+
@SuppressWarnings("unchecked")
60+
private static <T> T getPrivateField(Object obj, String fieldName)
61+
throws ReflectiveOperationException {
62+
java.lang.reflect.Field field = obj.getClass().getDeclaredField(fieldName);
63+
field.setAccessible(true);
64+
return (T) field.get(obj);
65+
}
66+
}

0 commit comments

Comments
 (0)