Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ ext {
jacksonVersion = '2.16.1'
junitVersion = '5.11.4'
slf4jVersion = '2.0.17'
langchainVersion = '1.9.1'
}

dependencies {
Expand Down Expand Up @@ -86,6 +87,14 @@ dependencies {
// Google GenAI Instrumentation
compileOnly "com.google.genai:google-genai:1.20.0"
testImplementation "com.google.genai:google-genai:1.20.0"

// LangChain4j Instrumentation
compileOnly "dev.langchain4j:langchain4j:${langchainVersion}"
compileOnly "dev.langchain4j:langchain4j-http-client:${langchainVersion}"
compileOnly "dev.langchain4j:langchain4j-open-ai:${langchainVersion}"
testImplementation "dev.langchain4j:langchain4j:${langchainVersion}"
testImplementation "dev.langchain4j:langchain4j-http-client:${langchainVersion}"
testImplementation "dev.langchain4j:langchain4j-open-ai:${langchainVersion}"
}

/**
Expand Down
18 changes: 17 additions & 1 deletion examples/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ dependencies {
implementation('org.springframework.boot:spring-boot-starter:3.4.1') {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
// to run langchain4j examples
implementation 'dev.langchain4j:langchain4j:1.9.1'
implementation 'dev.langchain4j:langchain4j-open-ai:1.9.1'
}

application {
Expand Down Expand Up @@ -142,7 +145,6 @@ task runSpringAI(type: JavaExec) {
}
}


task runRemoteEval(type: JavaExec) {
group = 'Braintrust SDK Examples'
description = 'Run the remote eval example'
Expand All @@ -156,3 +158,17 @@ task runRemoteEval(type: JavaExec) {
suspend = false
}
}

task runLangchain(type: JavaExec) {
group = 'Braintrust SDK Examples'
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'
classpath = sourceSets.main.runtimeClasspath
mainClass = 'dev.braintrust.examples.LangchainExample'
systemProperty 'org.slf4j.simpleLogger.log.dev.braintrust', braintrustLogLevel
debugOptions {
enabled = true
port = 5566
server = true
suspend = false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package dev.braintrust.examples;

import dev.braintrust.Braintrust;
import dev.braintrust.instrumentation.langchain.BraintrustLangchain;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;

/** Basic OTel + LangChain4j instrumentation example */
public class LangchainExample {

public static void main(String[] args) throws Exception {
if (null == System.getenv("OPENAI_API_KEY")) {
System.err.println(
"\nWARNING envar OPENAI_API_KEY not found. This example will likely fail.\n");
}
var braintrust = Braintrust.get();
var openTelemetry = braintrust.openTelemetryCreate();

ChatModel model =
BraintrustLangchain.wrap(
openTelemetry,
OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.temperature(0.0));

var rootSpan =
openTelemetry
.getTracer("my-instrumentation")
.spanBuilder("langchain4j-instrumentation-example")
.startSpan();
try (var ignored = rootSpan.makeCurrent()) {
chatExample(model);
} finally {
rootSpan.end();
}
var url =
braintrust.projectUri()
+ "/logs?r=%s&s=%s"
.formatted(
rootSpan.getSpanContext().getTraceId(),
rootSpan.getSpanContext().getSpanId());
System.out.println(
"\n\n Example complete! View your data in Braintrust: %s\n".formatted(url));
}

private static void chatExample(ChatModel model) {
var message = UserMessage.from("What is the capital of France?");
var response = model.chat(message);
System.out.println(
"\n~~~ LANGCHAIN4J CHAT RESPONSE: %s\n".formatted(response.aiMessage().text()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package dev.braintrust.instrumentation.langchain;

import dev.langchain4j.http.client.HttpClientBuilder;
import dev.langchain4j.http.client.HttpClientBuilderLoader;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import io.opentelemetry.api.OpenTelemetry;
import lombok.extern.slf4j.Slf4j;

/** Braintrust LangChain4j client instrumentation. */
@Slf4j
public final class BraintrustLangchain {
/** Instrument langchain openai chat model with braintrust traces */
public static OpenAiChatModel wrap(
OpenTelemetry otel, OpenAiChatModel.OpenAiChatModelBuilder builder) {
try {
HttpClientBuilder underlyingHttpClient = getPrivateField(builder, "httpClientBuilder");
if (underlyingHttpClient == null) {
underlyingHttpClient = HttpClientBuilderLoader.loadHttpClientBuilder();
}
HttpClientBuilder wrappedHttpClient =
wrap(otel, underlyingHttpClient, new Options("openai"));
return builder.httpClientBuilder(wrappedHttpClient).build();
} catch (Exception e) {
log.warn(
"Braintrust instrumentation could not be applied to OpenAiChatModel builder",
e);
return builder.build();
}
}

/** Instrument langchain openai chat model with braintrust traces */
public static OpenAiStreamingChatModel wrap(
OpenTelemetry otel, OpenAiStreamingChatModel.OpenAiStreamingChatModelBuilder builder) {
try {
HttpClientBuilder underlyingHttpClient = getPrivateField(builder, "httpClientBuilder");
if (underlyingHttpClient == null) {
underlyingHttpClient = HttpClientBuilderLoader.loadHttpClientBuilder();
}
HttpClientBuilder wrappedHttpClient =
wrap(otel, underlyingHttpClient, new Options("openai"));
return builder.httpClientBuilder(wrappedHttpClient).build();
} catch (Exception e) {
log.warn(
"Braintrust instrumentation could not be applied to OpenAiStreamingChatModel"
+ " builder",
e);
return builder.build();
}
}

private static HttpClientBuilder wrap(
OpenTelemetry otel, HttpClientBuilder builder, Options options) {
return new WrappedHttpClientBuilder(otel, builder, options);
}

public record Options(String providerName) {}

@SuppressWarnings("unchecked")
private static <T> T getPrivateField(Object obj, String fieldName)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This question is just for my own education: it looks like we're using reflection to access the private fields in order to instrument them, correct? What are the performance/stability risks associated with reflection? Are there other practical alternatives for instrumentation?

In the Ruby world, we generally would avoid accessing private fields because of the potential for instability (e.g. someone in a patch version changes the API.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's right, we're using reflection. There isn't much risk in this case because we'll just fail to apply instrumentation if something goes wrong

Performance is pretty good with reflection, but even if it wasn't this is only done once during client build

There isn't a viable alternative right now, but once we get into auto instrumentation for java we'll have more options

throws ReflectiveOperationException {
java.lang.reflect.Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return (T) field.get(obj);
}
}
Loading