From 88f2fd20ebcfb7ae74ddd1c874883c62719ddac6 Mon Sep 17 00:00:00 2001 From: Andrew Kent Date: Wed, 5 Nov 2025 10:34:26 -0700 Subject: [PATCH] refactor: simplify oai and anthropic instrumentation No changes in functionality. Removing dead code, improving naming, centralizing points where instrumentation is applied. --- .../anthropic/otel/AnthropicTelemetry.java | 9 +- .../otel/AnthropicTelemetryBuilder.java | 4 +- .../BraintrustAnthropicSpanAttributes.java | 47 ++++ .../anthropic/otel/GenAiAttributes.java | 18 -- .../otel/InstrumentedAnthropicClient.java | 9 +- .../otel/InstrumentedMessageService.java | 37 +-- .../otel/MessageAttributesGetter.java | 4 +- .../anthropic/otel/StreamListener.java | 9 +- .../otel/BraintrustOAISpanAttributes.java | 78 ++++++ .../openai/otel/ChatAttributesGetter.java | 4 +- .../otel/ChatCompletionEventsHelper.java | 227 ------------------ .../otel/EmbeddingAttributesGetter.java | 4 +- .../openai/otel/GenAiAttributes.java | 30 --- .../InstrumentedChatCompletionService.java | 29 +-- ...nstrumentedChatCompletionServiceAsync.java | 30 +-- .../openai/otel/InstrumentedChatService.java | 52 ---- .../otel/InstrumentedChatServiceAsync.java | 52 ---- .../openai/otel/InstrumentedOpenAiClient.java | 25 +- .../otel/InstrumentedOpenAiClientAsync.java | 25 +- .../openai/otel/OpenAITelemetry.java | 18 +- .../openai/otel/OpenAITelemetryBuilder.java | 5 +- .../openai/otel/StreamListener.java | 24 +- .../openai/otel/StreamedMessageBuffer.java | 37 --- 23 files changed, 209 insertions(+), 568 deletions(-) create mode 100644 src/main/java/dev/braintrust/instrumentation/anthropic/otel/BraintrustAnthropicSpanAttributes.java delete mode 100644 src/main/java/dev/braintrust/instrumentation/anthropic/otel/GenAiAttributes.java create mode 100644 src/main/java/dev/braintrust/instrumentation/openai/otel/BraintrustOAISpanAttributes.java delete mode 100644 src/main/java/dev/braintrust/instrumentation/openai/otel/ChatCompletionEventsHelper.java delete mode 100644 src/main/java/dev/braintrust/instrumentation/openai/otel/GenAiAttributes.java delete mode 100644 src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatService.java delete mode 100644 src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatServiceAsync.java diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/AnthropicTelemetry.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/AnthropicTelemetry.java index 3d918706..59f3a835 100644 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/AnthropicTelemetry.java +++ b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/AnthropicTelemetry.java @@ -4,7 +4,6 @@ import com.anthropic.models.messages.Message; import com.anthropic.models.messages.MessageCreateParams; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.logs.Logger; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; /** Entrypoint for instrumenting Anthropic clients. */ @@ -23,24 +22,18 @@ public static AnthropicTelemetryBuilder builder(OpenTelemetry openTelemetry) { } private final Instrumenter messageInstrumenter; - - private final Logger eventLogger; - private final boolean captureMessageContent; AnthropicTelemetry( Instrumenter messageInstrumenter, - Logger eventLogger, boolean captureMessageContent) { this.messageInstrumenter = messageInstrumenter; - this.eventLogger = eventLogger; this.captureMessageContent = captureMessageContent; } /** Wraps the provided AnthropicClient, enabling telemetry for it. */ public AnthropicClient wrap(AnthropicClient client) { - return new InstrumentedAnthropicClient( - client, messageInstrumenter, eventLogger, captureMessageContent) + return new InstrumentedAnthropicClient(client, messageInstrumenter, captureMessageContent) .createProxy(); } } diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/AnthropicTelemetryBuilder.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/AnthropicTelemetryBuilder.java index da683169..e4e7205e 100644 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/AnthropicTelemetryBuilder.java +++ b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/AnthropicTelemetryBuilder.java @@ -4,7 +4,6 @@ import com.anthropic.models.messages.MessageCreateParams; import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.logs.Logger; import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesExtractor; import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiClientMetrics; import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiSpanNameExtractor; @@ -50,7 +49,6 @@ public AnthropicTelemetry build() { .addOperationMetrics(GenAiClientMetrics.get()) .buildInstrumenter(SpanKindExtractor.alwaysClient()); - Logger eventLogger = openTelemetry.getLogsBridge().get(INSTRUMENTATION_NAME); - return new AnthropicTelemetry(messageInstrumenter, eventLogger, captureMessageContent); + return new AnthropicTelemetry(messageInstrumenter, captureMessageContent); } } diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/BraintrustAnthropicSpanAttributes.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/BraintrustAnthropicSpanAttributes.java new file mode 100644 index 00000000..4d01dbe3 --- /dev/null +++ b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/BraintrustAnthropicSpanAttributes.java @@ -0,0 +1,47 @@ +package dev.braintrust.instrumentation.anthropic.otel; + +import com.anthropic.models.messages.Message; +import com.anthropic.models.messages.MessageParam; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.opentelemetry.api.trace.Span; +import java.util.List; +import lombok.SneakyThrows; + +/** Centralized class for setting all Anthropic-related span attributes. */ +final class BraintrustAnthropicSpanAttributes { + private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + + // GenAI semantic convention constants + static final String OPERATION_CHAT = "chat"; + static final String SYSTEM_ANTHROPIC = "anthropic"; + + private BraintrustAnthropicSpanAttributes() {} + + /** + * Sets the braintrust.input_json attribute with the input messages. This captures the user's + * prompt and system messages before sending to Anthropic. + */ + @SneakyThrows + public static void setInputMessages(Span span, List messages) { + span.setAttribute("braintrust.input_json", JSON_MAPPER.writeValueAsString(messages)); + } + + /** + * Sets the braintrust.output_json attribute with the output message. This captures the + * assistant's response from Anthropic. + */ + @SneakyThrows + public static void setOutputMessage(Span span, Message message) { + span.setAttribute( + "braintrust.output_json", JSON_MAPPER.writeValueAsString(new Message[] {message})); + } + + /** + * Sets the braintrust.output_json attribute with a JSON array. This is used for streaming + * responses where the output is built incrementally. + */ + @SneakyThrows + public static void setOutputJson(Span span, String outputJson) { + span.setAttribute("braintrust.output_json", outputJson); + } +} diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/GenAiAttributes.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/GenAiAttributes.java deleted file mode 100644 index d328b687..00000000 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/GenAiAttributes.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.braintrust.instrumentation.anthropic.otel; - -// copied from GenAiIncubatingAttributes -final class GenAiAttributes { - static final class GenAiOperationNameIncubatingValues { - static final String CHAT = "chat"; - - private GenAiOperationNameIncubatingValues() {} - } - - static final class GenAiProviderNameIncubatingValues { - static final String ANTHROPIC = "anthropic"; - - private GenAiProviderNameIncubatingValues() {} - } - - private GenAiAttributes() {} -} diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedAnthropicClient.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedAnthropicClient.java index dece35fa..4d162719 100644 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedAnthropicClient.java +++ b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedAnthropicClient.java @@ -3,7 +3,6 @@ import com.anthropic.client.AnthropicClient; import com.anthropic.models.messages.Message; import com.anthropic.models.messages.MessageCreateParams; -import io.opentelemetry.api.logs.Logger; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import java.lang.reflect.Method; @@ -11,17 +10,14 @@ final class InstrumentedAnthropicClient extends DelegatingInvocationHandler { private final Instrumenter messageInstrumenter; - private final Logger eventLogger; private final boolean captureMessageContent; InstrumentedAnthropicClient( AnthropicClient delegate, Instrumenter messageInstrumenter, - Logger eventLogger, boolean captureMessageContent) { super(delegate); this.messageInstrumenter = messageInstrumenter; - this.eventLogger = eventLogger; this.captureMessageContent = captureMessageContent; } @@ -36,10 +32,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl Class[] parameterTypes = method.getParameterTypes(); if (methodName.equals("messages") && parameterTypes.length == 0) { return new InstrumentedMessageService( - delegate.messages(), - messageInstrumenter, - eventLogger, - captureMessageContent) + delegate.messages(), messageInstrumenter, captureMessageContent) .createProxy(); } return super.invoke(proxy, method, args); diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedMessageService.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedMessageService.java index 21d631a7..4e33900d 100644 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedMessageService.java +++ b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/InstrumentedMessageService.java @@ -8,8 +8,6 @@ import com.anthropic.models.messages.RawMessageStreamEvent; import com.anthropic.models.messages.TextBlockParam; import com.anthropic.services.blocking.MessageService; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.opentelemetry.api.logs.Logger; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; @@ -18,24 +16,19 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; -import lombok.SneakyThrows; final class InstrumentedMessageService extends DelegatingInvocationHandler { - private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); private final Instrumenter instrumenter; - private final Logger eventLogger; private final boolean captureMessageContent; InstrumentedMessageService( MessageService delegate, Instrumenter instrumenter, - Logger eventLogger, boolean captureMessageContent) { super(delegate); this.instrumenter = instrumenter; - this.eventLogger = eventLogger; this.captureMessageContent = captureMessageContent; } @@ -79,7 +72,6 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl return super.invoke(proxy, method, args); } - @SneakyThrows private Message create(MessageCreateParams inputMessage, RequestOptions requestOptions) { Context parentContext = Context.current(); if (!instrumenter.shouldStart(parentContext, inputMessage)) { @@ -99,14 +91,9 @@ private Message create(MessageCreateParams inputMessage, RequestOptions requestO .content(inputMessage.system().get().asString()) .build()); } - Span.current() - .setAttribute( - "braintrust.input_json", JSON_MAPPER.writeValueAsString(inputMessages)); + BraintrustAnthropicSpanAttributes.setInputMessages(Span.current(), inputMessages); outputMessage = delegate.create(inputMessage, requestOptions); - Span.current() - .setAttribute( - "braintrust.output_json", - JSON_MAPPER.writeValueAsString(new Message[] {outputMessage})); + BraintrustAnthropicSpanAttributes.setOutputMessage(Span.current(), outputMessage); } catch (Throwable t) { instrumenter.end(context, inputMessage, null, t); throw t; @@ -120,20 +107,20 @@ private StreamResponse createStreaming( MessageCreateParams inputMessage, RequestOptions requestOptions) { Context parentContext = Context.current(); if (!instrumenter.shouldStart(parentContext, inputMessage)) { - return createStreamingWithLogs(parentContext, inputMessage, requestOptions, false); + return createStreamingWithAttributes( + parentContext, inputMessage, requestOptions, false); } Context context = instrumenter.start(parentContext, inputMessage); try (Scope ignored = context.makeCurrent()) { - return createStreamingWithLogs(context, inputMessage, requestOptions, true); + return createStreamingWithAttributes(context, inputMessage, requestOptions, true); } catch (Throwable t) { instrumenter.end(context, inputMessage, null, t); throw t; } } - @SneakyThrows - private StreamResponse createStreamingWithLogs( + private StreamResponse createStreamingWithAttributes( Context context, MessageCreateParams inputMessage, RequestOptions requestOptions, @@ -148,21 +135,15 @@ private StreamResponse createStreamingWithLogs( .content(inputMessage.system().get().asString()) .build()); } - Span.fromContext(context) - .setAttribute( - "braintrust.input_json", JSON_MAPPER.writeValueAsString(inputMessages)); + BraintrustAnthropicSpanAttributes.setInputMessages( + Span.fromContext(context), inputMessages); StreamResponse result = delegate.createStreaming(inputMessage, requestOptions); return new TracingStreamedResponse( result, new StreamListener( - context, - inputMessage, - instrumenter, - eventLogger, - captureMessageContent, - newSpan)); + context, inputMessage, instrumenter, captureMessageContent, newSpan)); } private static String contentToString(MessageCreateParams.System content) { diff --git a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/MessageAttributesGetter.java b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/MessageAttributesGetter.java index a3477514..a531d4c8 100644 --- a/src/main/java/dev/braintrust/instrumentation/anthropic/otel/MessageAttributesGetter.java +++ b/src/main/java/dev/braintrust/instrumentation/anthropic/otel/MessageAttributesGetter.java @@ -13,12 +13,12 @@ enum MessageAttributesGetter implements GenAiAttributesGetter instrumenter; - private final Logger eventLogger; private final boolean captureMessageContent; private final boolean newSpan; private final AtomicBoolean hasEnded; @@ -42,13 +40,11 @@ final class StreamListener { Context context, MessageCreateParams request, Instrumenter instrumenter, - Logger eventLogger, boolean captureMessageContent, boolean newSpan) { this.context = context; this.request = request; this.instrumenter = instrumenter; - this.eventLogger = eventLogger; this.captureMessageContent = captureMessageContent; this.newSpan = newSpan; hasEnded = new AtomicBoolean(); @@ -93,9 +89,8 @@ void onEvent(RawMessageStreamEvent event) { message.put("content", contentBuilder.toString()); outputArray.add(message); - Span.fromContext(context) - .setAttribute( - "braintrust.output_json", JSON_MAPPER.writeValueAsString(outputArray)); + BraintrustAnthropicSpanAttributes.setOutputJson( + Span.fromContext(context), JSON_MAPPER.writeValueAsString(outputArray)); } } diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/BraintrustOAISpanAttributes.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/BraintrustOAISpanAttributes.java new file mode 100644 index 00000000..d48635c8 --- /dev/null +++ b/src/main/java/dev/braintrust/instrumentation/openai/otel/BraintrustOAISpanAttributes.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package dev.braintrust.instrumentation.openai.otel; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.openai.models.chat.completions.ChatCompletion; +import com.openai.models.chat.completions.ChatCompletionMessage; +import io.opentelemetry.api.trace.Span; +import java.util.List; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +/** Centralized class for setting all OpenAI-related span attributes. */ +@Slf4j +final class BraintrustOAISpanAttributes { + private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + + // GenAI semantic convention constants + static final String OPERATION_CHAT = "chat"; + static final String OPERATION_EMBEDDINGS = "embeddings"; + static final String SYSTEM_OPENAI = "openai"; + + private BraintrustOAISpanAttributes() {} + + /** + * Sets the gen_ai.input.messages attribute with the serialized input messages. This captures + * the user's prompt and system messages before sending to OpenAI. + */ + @SneakyThrows + public static void setInputMessages(Span span, List messages) { + String semconvJson = + GenAiSemconvSerializer.serializeInputMessages( + (List) + messages); + span.setAttribute("gen_ai.input.messages", semconvJson); + } + + /** + * Sets the gen_ai.output.messages attribute with the serialized output message. This captures + * the assistant's response from OpenAI for a single choice. + */ + @SneakyThrows + public static void setOutputMessages( + Span span, ChatCompletionMessage message, String finishReason) { + String outputJson = GenAiSemconvSerializer.serializeOutputMessage(message, finishReason); + span.setAttribute("gen_ai.output.messages", outputJson); + } + + /** + * Sets the gen_ai.output.messages attribute for the primary choice in a completion. Logs a + * debug message if there are no choices or multiple choices. + */ + public static void setOutputMessagesFromCompletion(Span span, ChatCompletion completion) { + if (completion.choices().isEmpty()) { + log.debug("no choices in OAI response"); + } else if (completion.choices().size() > 1) { + log.debug("multiple choices in OAI response: {}", completion.choices().size()); + } else { + // Set gen_ai.output.messages attribute for single choice (most common case) + ChatCompletion.Choice choice = completion.choices().get(0); + setOutputMessages(span, choice.message(), choice.finishReason().toString()); + } + } + + /** + * Sets the braintrust.output_json attribute with a single message. This is used for streaming + * responses to capture output in Braintrust format. + */ + @SneakyThrows + public static void setBraintrustOutputJson(Span span, ChatCompletionMessage message) { + span.setAttribute( + "braintrust.output_json", + JSON_MAPPER.writeValueAsString(new ChatCompletionMessage[] {message})); + } +} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatAttributesGetter.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatAttributesGetter.java index 110a2790..f82e5ace 100644 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatAttributesGetter.java +++ b/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatAttributesGetter.java @@ -22,12 +22,12 @@ enum ChatAttributesGetter @Override public String getOperationName(ChatCompletionCreateParams request) { - return GenAiAttributes.GenAiOperationNameIncubatingValues.CHAT; + return BraintrustOAISpanAttributes.OPERATION_CHAT; } @Override public String getSystem(ChatCompletionCreateParams request) { - return GenAiAttributes.GenAiProviderNameIncubatingValues.OPENAI; + return BraintrustOAISpanAttributes.SYSTEM_OPENAI; } @Override diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatCompletionEventsHelper.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatCompletionEventsHelper.java deleted file mode 100644 index f2b86314..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatCompletionEventsHelper.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import static io.opentelemetry.api.common.AttributeKey.stringKey; - -import com.openai.models.chat.completions.ChatCompletion; -import com.openai.models.chat.completions.ChatCompletionAssistantMessageParam; -import com.openai.models.chat.completions.ChatCompletionContentPartText; -import com.openai.models.chat.completions.ChatCompletionCreateParams; -import com.openai.models.chat.completions.ChatCompletionDeveloperMessageParam; -import com.openai.models.chat.completions.ChatCompletionMessage; -import com.openai.models.chat.completions.ChatCompletionMessageToolCall; -import com.openai.models.chat.completions.ChatCompletionSystemMessageParam; -import com.openai.models.chat.completions.ChatCompletionToolMessageParam; -import com.openai.models.chat.completions.ChatCompletionUserMessageParam; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Value; -import io.opentelemetry.api.logs.LogRecordBuilder; -import io.opentelemetry.api.logs.Logger; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.context.Context; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -final class ChatCompletionEventsHelper { - - private static final AttributeKey EVENT_NAME = stringKey("event.name"); - - @SneakyThrows - public static void emitPromptLogEvents( - Context context, - Logger eventLogger, - ChatCompletionCreateParams request, - boolean captureMessageContent) { - String semconvJson = GenAiSemconvSerializer.serializeInputMessages(request.messages()); - Span span = Span.current(); - span.setAttribute("gen_ai.input.messages", semconvJson); - } - - private static String contentToString(ChatCompletionToolMessageParam.Content content) { - if (content.isText()) { - return content.asText(); - } else if (content.isArrayOfContentParts()) { - return joinContentParts(content.asArrayOfContentParts()); - } else { - return ""; - } - } - - private static String contentToString(ChatCompletionAssistantMessageParam.Content content) { - if (content.isText()) { - return content.asText(); - } else if (content.isArrayOfContentParts()) { - return content.asArrayOfContentParts().stream() - .map( - part -> { - if (part.isText()) { - return part.asText().text(); - } - if (part.isRefusal()) { - return part.asRefusal().refusal(); - } - return null; - }) - .filter(Objects::nonNull) - .collect(Collectors.joining()); - } else { - return ""; - } - } - - private static String contentToString(ChatCompletionSystemMessageParam.Content content) { - if (content.isText()) { - return content.asText(); - } else if (content.isArrayOfContentParts()) { - return joinContentParts(content.asArrayOfContentParts()); - } else { - return ""; - } - } - - private static String contentToString(ChatCompletionDeveloperMessageParam.Content content) { - if (content.isText()) { - return content.asText(); - } else if (content.isArrayOfContentParts()) { - return joinContentParts(content.asArrayOfContentParts()); - } else { - return ""; - } - } - - private static String contentToString(ChatCompletionUserMessageParam.Content content) { - if (content.isText()) { - return content.asText(); - } else if (content.isArrayOfContentParts()) { - return content.asArrayOfContentParts().stream() - .map(part -> part.isText() ? part.asText().text() : null) - .filter(Objects::nonNull) - .collect(Collectors.joining()); - } else { - return ""; - } - } - - private static String joinContentParts(List contentParts) { - return contentParts.stream() - .map(ChatCompletionContentPartText::text) - .collect(Collectors.joining()); - } - - @SneakyThrows - public static void emitCompletionLogEvents( - Context context, - Logger eventLogger, - ChatCompletion completion, - boolean captureMessageContent) { - if (completion.choices().isEmpty()) { - log.debug("no choices in OAI response"); - } else if (completion.choices().size() > 1) { - log.debug("multiple choices in OAI response: {}", completion.choices().size()); - } else { - // Set gen_ai.output.messages attribute for single choice (most common case) - ChatCompletion.Choice choice = completion.choices().get(0); - String outputJson = - GenAiSemconvSerializer.serializeOutputMessage( - choice.message(), choice.finishReason().toString()); - Span.current().setAttribute("gen_ai.output.messages", outputJson); - } - for (ChatCompletion.Choice choice : completion.choices()) { - ChatCompletionMessage choiceMsg = choice.message(); - Map> message = new HashMap<>(); - if (captureMessageContent) { - choiceMsg - .content() - .ifPresent( - content -> { - message.put("content", Value.of(content)); - }); - } - choiceMsg - .toolCalls() - .ifPresent( - toolCalls -> { - message.put( - "tool_calls", - Value.of( - toolCalls.stream() - .map( - call -> - buildToolCallEventObject( - call, - captureMessageContent)) - .collect(Collectors.toList()))); - }); - emitCompletionLogEvent( - context, - eventLogger, - choice.index(), - choice.finishReason().toString(), - Value.of(message)); - } - } - - public static void emitCompletionLogEvent( - Context context, - Logger eventLogger, - long index, - String finishReason, - Value eventMessageObject) { - Map> body = new HashMap<>(); - body.put("finish_reason", Value.of(finishReason)); - body.put("index", Value.of(index)); - body.put("message", eventMessageObject); - // newEvent(eventLogger, - // "gen_ai.choice").setContext(context).setBody(Value.of(body)).emit(); - } - - private static LogRecordBuilder newEvent(Logger eventLogger, String name) { - // NOTE: disabling logger events in braintrust instrumentation. We don't use these events. - // Will have to properly hanlde this if we want to merge braintrust attributes upstream into - // otel instrumentation - /* - return eventLogger - .logRecordBuilder() - .setAttribute(EVENT_NAME, name) - .setAttribute(GEN_AI_PROVIDER_NAME, "openai"); - */ - throw new RuntimeException("Should not invoke"); - } - - private static Value buildToolCallEventObject( - ChatCompletionMessageToolCall call, boolean captureMessageContent) { - Map> result = new HashMap<>(); - GenAiSemconvSerializer.FunctionAccess functionAccess = - GenAiSemconvSerializer.getFunctionAccess(call); - if (functionAccess != null) { - result.put("id", Value.of(functionAccess.id())); - result.put( - "type", - Value.of("function")); // "function" is the only currently supported type - result.put("function", buildFunctionEventObject(functionAccess, captureMessageContent)); - } - return Value.of(result); - } - - private static Value buildFunctionEventObject( - GenAiSemconvSerializer.FunctionAccess functionAccess, boolean captureMessageContent) { - Map> result = new HashMap<>(); - result.put("name", Value.of(functionAccess.name())); - if (captureMessageContent) { - result.put("arguments", Value.of(functionAccess.arguments())); - } - return Value.of(result); - } - - private ChatCompletionEventsHelper() {} -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/EmbeddingAttributesGetter.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/EmbeddingAttributesGetter.java index 220e23f4..d9f33b8e 100644 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/EmbeddingAttributesGetter.java +++ b/src/main/java/dev/braintrust/instrumentation/openai/otel/EmbeddingAttributesGetter.java @@ -20,12 +20,12 @@ enum EmbeddingAttributesGetter @Override public String getOperationName(EmbeddingCreateParams request) { - return GenAiAttributes.GenAiOperationNameIncubatingValues.EMBEDDINGS; + return BraintrustOAISpanAttributes.OPERATION_EMBEDDINGS; } @Override public String getSystem(EmbeddingCreateParams request) { - return GenAiAttributes.GenAiProviderNameIncubatingValues.OPENAI; + return BraintrustOAISpanAttributes.SYSTEM_OPENAI; } @Override diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/GenAiAttributes.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/GenAiAttributes.java deleted file mode 100644 index 690aca56..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/GenAiAttributes.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import static io.opentelemetry.api.common.AttributeKey.stringKey; - -import io.opentelemetry.api.common.AttributeKey; - -// copied from GenAiIncubatingAttributes -final class GenAiAttributes { - static final AttributeKey GEN_AI_PROVIDER_NAME = stringKey("gen_ai.provider.name"); - - static final class GenAiOperationNameIncubatingValues { - static final String CHAT = "chat"; - static final String EMBEDDINGS = "embeddings"; - - private GenAiOperationNameIncubatingValues() {} - } - - static final class GenAiProviderNameIncubatingValues { - static final String OPENAI = "openai"; - - private GenAiProviderNameIncubatingValues() {} - } - - private GenAiAttributes() {} -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatCompletionService.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatCompletionService.java index b80d1df3..8361cf67 100644 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatCompletionService.java +++ b/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatCompletionService.java @@ -11,7 +11,7 @@ import com.openai.models.chat.completions.ChatCompletionChunk; import com.openai.models.chat.completions.ChatCompletionCreateParams; import com.openai.services.blocking.chat.ChatCompletionService; -import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; @@ -22,17 +22,14 @@ final class InstrumentedChatCompletionService ChatCompletionService, InstrumentedChatCompletionService> { private final Instrumenter instrumenter; - private final Logger eventLogger; private final boolean captureMessageContent; InstrumentedChatCompletionService( ChatCompletionService delegate, Instrumenter instrumenter, - Logger eventLogger, boolean captureMessageContent) { super(delegate); this.instrumenter = instrumenter; - this.eventLogger = eventLogger; this.captureMessageContent = captureMessageContent; } @@ -83,13 +80,13 @@ private ChatCompletion create( ChatCompletionCreateParams chatCompletionCreateParams, RequestOptions requestOptions) { Context parentContext = Context.current(); if (!instrumenter.shouldStart(parentContext, chatCompletionCreateParams)) { - return createWithLogs(parentContext, chatCompletionCreateParams, requestOptions); + return createWithAttributes(parentContext, chatCompletionCreateParams, requestOptions); } Context context = instrumenter.start(parentContext, chatCompletionCreateParams); ChatCompletion completion; try (Scope ignored = context.makeCurrent()) { - completion = createWithLogs(context, chatCompletionCreateParams, requestOptions); + completion = createWithAttributes(context, chatCompletionCreateParams, requestOptions); } catch (Throwable t) { instrumenter.end(context, chatCompletionCreateParams, null, t); throw t; @@ -99,15 +96,14 @@ private ChatCompletion create( return completion; } - private ChatCompletion createWithLogs( + private ChatCompletion createWithAttributes( Context context, ChatCompletionCreateParams chatCompletionCreateParams, RequestOptions requestOptions) { - ChatCompletionEventsHelper.emitPromptLogEvents( - context, eventLogger, chatCompletionCreateParams, captureMessageContent); + BraintrustOAISpanAttributes.setInputMessages( + Span.current(), chatCompletionCreateParams.messages()); ChatCompletion result = delegate.create(chatCompletionCreateParams, requestOptions); - ChatCompletionEventsHelper.emitCompletionLogEvents( - context, eventLogger, result, captureMessageContent); + BraintrustOAISpanAttributes.setOutputMessagesFromCompletion(Span.current(), result); return result; } @@ -115,13 +111,13 @@ private StreamResponse createStreaming( ChatCompletionCreateParams chatCompletionCreateParams, RequestOptions requestOptions) { Context parentContext = Context.current(); if (!instrumenter.shouldStart(parentContext, chatCompletionCreateParams)) { - return createStreamingWithLogs( + return createStreamingWithAttributes( parentContext, chatCompletionCreateParams, requestOptions, false); } Context context = instrumenter.start(parentContext, chatCompletionCreateParams); try (Scope ignored = context.makeCurrent()) { - return createStreamingWithLogs( + return createStreamingWithAttributes( context, chatCompletionCreateParams, requestOptions, true); } catch (Throwable t) { instrumenter.end(context, chatCompletionCreateParams, null, t); @@ -129,13 +125,13 @@ private StreamResponse createStreaming( } } - private StreamResponse createStreamingWithLogs( + private StreamResponse createStreamingWithAttributes( Context context, ChatCompletionCreateParams chatCompletionCreateParams, RequestOptions requestOptions, boolean newSpan) { - ChatCompletionEventsHelper.emitPromptLogEvents( - context, eventLogger, chatCompletionCreateParams, captureMessageContent); + BraintrustOAISpanAttributes.setInputMessages( + Span.current(), chatCompletionCreateParams.messages()); StreamResponse result = delegate.createStreaming(chatCompletionCreateParams, requestOptions); return new TracingStreamedResponse( @@ -144,7 +140,6 @@ private StreamResponse createStreamingWithLogs( context, chatCompletionCreateParams, instrumenter, - eventLogger, captureMessageContent, newSpan)); } diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatCompletionServiceAsync.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatCompletionServiceAsync.java index 420dec7c..2885b80e 100644 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatCompletionServiceAsync.java +++ b/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatCompletionServiceAsync.java @@ -11,7 +11,7 @@ import com.openai.models.chat.completions.ChatCompletionChunk; import com.openai.models.chat.completions.ChatCompletionCreateParams; import com.openai.services.async.chat.ChatCompletionServiceAsync; -import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; @@ -23,17 +23,14 @@ final class InstrumentedChatCompletionServiceAsync ChatCompletionServiceAsync, InstrumentedChatCompletionServiceAsync> { private final Instrumenter instrumenter; - private final Logger eventLogger; private final boolean captureMessageContent; InstrumentedChatCompletionServiceAsync( ChatCompletionServiceAsync delegate, Instrumenter instrumenter, - Logger eventLogger, boolean captureMessageContent) { super(delegate); this.instrumenter = instrumenter; - this.eventLogger = eventLogger; this.captureMessageContent = captureMessageContent; } @@ -84,13 +81,13 @@ private CompletableFuture create( ChatCompletionCreateParams chatCompletionCreateParams, RequestOptions requestOptions) { Context parentContext = Context.current(); if (!instrumenter.shouldStart(parentContext, chatCompletionCreateParams)) { - return createWithLogs(parentContext, chatCompletionCreateParams, requestOptions); + return createWithAttributes(parentContext, chatCompletionCreateParams, requestOptions); } Context context = instrumenter.start(parentContext, chatCompletionCreateParams); CompletableFuture future; try (Scope ignored = context.makeCurrent()) { - future = createWithLogs(context, chatCompletionCreateParams, requestOptions); + future = createWithAttributes(context, chatCompletionCreateParams, requestOptions); } catch (Throwable t) { instrumenter.end(context, chatCompletionCreateParams, null, t); throw t; @@ -102,18 +99,18 @@ private CompletableFuture create( return CompletableFutureWrapper.wrap(future, parentContext); } - private CompletableFuture createWithLogs( + private CompletableFuture createWithAttributes( Context context, ChatCompletionCreateParams chatCompletionCreateParams, RequestOptions requestOptions) { - ChatCompletionEventsHelper.emitPromptLogEvents( - context, eventLogger, chatCompletionCreateParams, captureMessageContent); + BraintrustOAISpanAttributes.setInputMessages( + Span.current(), chatCompletionCreateParams.messages()); CompletableFuture future = delegate.create(chatCompletionCreateParams, requestOptions); future.thenAccept( r -> - ChatCompletionEventsHelper.emitCompletionLogEvents( - context, eventLogger, r, captureMessageContent)); + BraintrustOAISpanAttributes.setOutputMessagesFromCompletion( + Span.current(), r)); return future; } @@ -121,13 +118,13 @@ private AsyncStreamResponse createStreaming( ChatCompletionCreateParams chatCompletionCreateParams, RequestOptions requestOptions) { Context parentContext = Context.current(); if (!instrumenter.shouldStart(parentContext, chatCompletionCreateParams)) { - return createStreamingWithLogs( + return createStreamingWithAttributes( parentContext, chatCompletionCreateParams, requestOptions, false); } Context context = instrumenter.start(parentContext, chatCompletionCreateParams); try (Scope ignored = context.makeCurrent()) { - return createStreamingWithLogs( + return createStreamingWithAttributes( context, chatCompletionCreateParams, requestOptions, true); } catch (Throwable t) { instrumenter.end(context, chatCompletionCreateParams, null, t); @@ -135,13 +132,13 @@ private AsyncStreamResponse createStreaming( } } - private AsyncStreamResponse createStreamingWithLogs( + private AsyncStreamResponse createStreamingWithAttributes( Context context, ChatCompletionCreateParams chatCompletionCreateParams, RequestOptions requestOptions, boolean newSpan) { - ChatCompletionEventsHelper.emitPromptLogEvents( - context, eventLogger, chatCompletionCreateParams, captureMessageContent); + BraintrustOAISpanAttributes.setInputMessages( + Span.current(), chatCompletionCreateParams.messages()); AsyncStreamResponse result = delegate.createStreaming(chatCompletionCreateParams, requestOptions); return new TracingAsyncStreamedResponse( @@ -150,7 +147,6 @@ private AsyncStreamResponse createStreamingWithLogs( context, chatCompletionCreateParams, instrumenter, - eventLogger, captureMessageContent, newSpan)); } diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatService.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatService.java deleted file mode 100644 index 7165fb14..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatService.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import com.openai.models.chat.completions.ChatCompletion; -import com.openai.models.chat.completions.ChatCompletionCreateParams; -import com.openai.services.blocking.ChatService; -import io.opentelemetry.api.logs.Logger; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import java.lang.reflect.Method; - -final class InstrumentedChatService - extends DelegatingInvocationHandler { - - private final Instrumenter instrumenter; - private final Logger eventLogger; - private final boolean captureMessageContent; - - InstrumentedChatService( - ChatService delegate, - Instrumenter instrumenter, - Logger eventLogger, - boolean captureMessageContent) { - super(delegate); - this.instrumenter = instrumenter; - this.eventLogger = eventLogger; - this.captureMessageContent = captureMessageContent; - } - - @Override - protected Class getProxyType() { - return ChatService.class; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String methodName = method.getName(); - Class[] parameterTypes = method.getParameterTypes(); - if (methodName.equals("completions") && parameterTypes.length == 0) { - return new InstrumentedChatCompletionService( - delegate.completions(), - instrumenter, - eventLogger, - captureMessageContent) - .createProxy(); - } - return super.invoke(proxy, method, args); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatServiceAsync.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatServiceAsync.java deleted file mode 100644 index c7e0799a..00000000 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedChatServiceAsync.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package dev.braintrust.instrumentation.openai.otel; - -import com.openai.models.chat.completions.ChatCompletion; -import com.openai.models.chat.completions.ChatCompletionCreateParams; -import com.openai.services.async.ChatServiceAsync; -import io.opentelemetry.api.logs.Logger; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import java.lang.reflect.Method; - -final class InstrumentedChatServiceAsync - extends DelegatingInvocationHandler { - - private final Instrumenter instrumenter; - private final Logger eventLogger; - private final boolean captureMessageContent; - - InstrumentedChatServiceAsync( - ChatServiceAsync delegate, - Instrumenter instrumenter, - Logger eventLogger, - boolean captureMessageContent) { - super(delegate); - this.instrumenter = instrumenter; - this.eventLogger = eventLogger; - this.captureMessageContent = captureMessageContent; - } - - @Override - protected Class getProxyType() { - return ChatServiceAsync.class; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String methodName = method.getName(); - Class[] parameterTypes = method.getParameterTypes(); - if (methodName.equals("completions") && parameterTypes.length == 0) { - return new InstrumentedChatCompletionServiceAsync( - delegate.completions(), - instrumenter, - eventLogger, - captureMessageContent) - .createProxy(); - } - return super.invoke(proxy, method, args); - } -} diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedOpenAiClient.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedOpenAiClient.java index 7396b0d0..0391c54e 100644 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedOpenAiClient.java +++ b/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedOpenAiClient.java @@ -10,7 +10,6 @@ import com.openai.models.chat.completions.ChatCompletionCreateParams; import com.openai.models.embeddings.CreateEmbeddingResponse; import com.openai.models.embeddings.EmbeddingCreateParams; -import io.opentelemetry.api.logs.Logger; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import java.lang.reflect.Method; @@ -20,19 +19,16 @@ final class InstrumentedOpenAiClient private final Instrumenter chatInstrumenter; private final Instrumenter embeddingInstrumenter; - private final Logger eventLogger; private final boolean captureMessageContent; InstrumentedOpenAiClient( OpenAIClient delegate, Instrumenter chatInstrumenter, Instrumenter embeddingInstrumenter, - Logger eventLogger, boolean captureMessageContent) { super(delegate); this.chatInstrumenter = chatInstrumenter; this.embeddingInstrumenter = embeddingInstrumenter; - this.eventLogger = eventLogger; this.captureMessageContent = captureMessageContent; } @@ -46,9 +42,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl String methodName = method.getName(); Class[] parameterTypes = method.getParameterTypes(); if (methodName.equals("chat") && parameterTypes.length == 0) { - return new InstrumentedChatService( - delegate.chat(), chatInstrumenter, eventLogger, captureMessageContent) - .createProxy(); + return createChatServiceProxy(); } if (methodName.equals("embeddings") && parameterTypes.length == 0) { return new InstrumentedEmbeddingService(delegate.embeddings(), embeddingInstrumenter) @@ -59,10 +53,25 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl delegate.async(), chatInstrumenter, embeddingInstrumenter, - eventLogger, captureMessageContent) .createProxy(); } return super.invoke(proxy, method, args); } + + private Object createChatServiceProxy() { + return java.lang.reflect.Proxy.newProxyInstance( + com.openai.services.blocking.ChatService.class.getClassLoader(), + new Class[] {com.openai.services.blocking.ChatService.class}, + (p, m, a) -> { + if ("completions".equals(m.getName()) && m.getParameterCount() == 0) { + return new InstrumentedChatCompletionService( + delegate.chat().completions(), + chatInstrumenter, + captureMessageContent) + .createProxy(); + } + return m.invoke(delegate.chat(), a); + }); + } } diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedOpenAiClientAsync.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedOpenAiClientAsync.java index 9988f8dc..48999b20 100644 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedOpenAiClientAsync.java +++ b/src/main/java/dev/braintrust/instrumentation/openai/otel/InstrumentedOpenAiClientAsync.java @@ -10,7 +10,6 @@ import com.openai.models.chat.completions.ChatCompletionCreateParams; import com.openai.models.embeddings.CreateEmbeddingResponse; import com.openai.models.embeddings.EmbeddingCreateParams; -import io.opentelemetry.api.logs.Logger; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import java.lang.reflect.Method; @@ -20,19 +19,16 @@ final class InstrumentedOpenAiClientAsync private final Instrumenter chatInstrumenter; private final Instrumenter embeddingInstrumenter; - private final Logger eventLogger; private final boolean captureMessageContent; InstrumentedOpenAiClientAsync( OpenAIClientAsync delegate, Instrumenter chatInstrumenter, Instrumenter embeddingInstrumenter, - Logger eventLogger, boolean captureMessageContent) { super(delegate); this.chatInstrumenter = chatInstrumenter; this.embeddingInstrumenter = embeddingInstrumenter; - this.eventLogger = eventLogger; this.captureMessageContent = captureMessageContent; } @@ -46,9 +42,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl String methodName = method.getName(); Class[] parameterTypes = method.getParameterTypes(); if (methodName.equals("chat") && parameterTypes.length == 0) { - return new InstrumentedChatServiceAsync( - delegate.chat(), chatInstrumenter, eventLogger, captureMessageContent) - .createProxy(); + return createChatServiceAsyncProxy(); } if (methodName.equals("embeddings") && parameterTypes.length == 0) { return new InstrumentedEmbeddingServiceAsync( @@ -60,10 +54,25 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl delegate.sync(), chatInstrumenter, embeddingInstrumenter, - eventLogger, captureMessageContent) .createProxy(); } return super.invoke(proxy, method, args); } + + private Object createChatServiceAsyncProxy() { + return java.lang.reflect.Proxy.newProxyInstance( + com.openai.services.async.ChatServiceAsync.class.getClassLoader(), + new Class[] {com.openai.services.async.ChatServiceAsync.class}, + (p, m, a) -> { + if ("completions".equals(m.getName()) && m.getParameterCount() == 0) { + return new InstrumentedChatCompletionServiceAsync( + delegate.chat().completions(), + chatInstrumenter, + captureMessageContent) + .createProxy(); + } + return m.invoke(delegate.chat(), a); + }); + } } diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/OpenAITelemetry.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/OpenAITelemetry.java index db65615f..64f858ec 100644 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/OpenAITelemetry.java +++ b/src/main/java/dev/braintrust/instrumentation/openai/otel/OpenAITelemetry.java @@ -12,7 +12,6 @@ import com.openai.models.embeddings.CreateEmbeddingResponse; import com.openai.models.embeddings.EmbeddingCreateParams; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.logs.Logger; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; /** Entrypoint for instrumenting OpenAI clients. */ @@ -33,41 +32,28 @@ public static OpenAITelemetryBuilder builder(OpenTelemetry openTelemetry) { private final Instrumenter chatInstrumenter; private final Instrumenter embeddingsInstrumenter; - - private final Logger eventLogger; - private final boolean captureMessageContent; OpenAITelemetry( Instrumenter chatInstrumenter, Instrumenter embeddingsInstrumenter, - Logger eventLogger, boolean captureMessageContent) { this.chatInstrumenter = chatInstrumenter; this.embeddingsInstrumenter = embeddingsInstrumenter; - this.eventLogger = eventLogger; this.captureMessageContent = captureMessageContent; } /** Wraps the provided OpenAIClient, enabling telemetry for it. */ public OpenAIClient wrap(OpenAIClient client) { return new InstrumentedOpenAiClient( - client, - chatInstrumenter, - embeddingsInstrumenter, - eventLogger, - captureMessageContent) + client, chatInstrumenter, embeddingsInstrumenter, captureMessageContent) .createProxy(); } /** Wraps the provided OpenAIClientAsync, enabling telemetry for it. */ public OpenAIClientAsync wrap(OpenAIClientAsync client) { return new InstrumentedOpenAiClientAsync( - client, - chatInstrumenter, - embeddingsInstrumenter, - eventLogger, - captureMessageContent) + client, chatInstrumenter, embeddingsInstrumenter, captureMessageContent) .createProxy(); } } diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/OpenAITelemetryBuilder.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/OpenAITelemetryBuilder.java index 9a258667..077b604a 100644 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/OpenAITelemetryBuilder.java +++ b/src/main/java/dev/braintrust/instrumentation/openai/otel/OpenAITelemetryBuilder.java @@ -11,7 +11,6 @@ import com.openai.models.embeddings.CreateEmbeddingResponse; import com.openai.models.embeddings.EmbeddingCreateParams; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.logs.Logger; import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesExtractor; import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiClientMetrics; import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiSpanNameExtractor; @@ -68,8 +67,6 @@ public OpenAITelemetry build() { .addOperationMetrics(GenAiClientMetrics.get()) .buildInstrumenter(SpanKindExtractor.alwaysClient()); - Logger eventLogger = openTelemetry.getLogsBridge().get(INSTRUMENTATION_NAME); - return new OpenAITelemetry( - chatInstrumenter, embeddingsInstrumenter, eventLogger, captureMessageContent); + return new OpenAITelemetry(chatInstrumenter, embeddingsInstrumenter, captureMessageContent); } } diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/StreamListener.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/StreamListener.java index cc574364..16b3828d 100644 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/StreamListener.java +++ b/src/main/java/dev/braintrust/instrumentation/openai/otel/StreamListener.java @@ -5,13 +5,10 @@ package dev.braintrust.instrumentation.openai.otel; -import com.fasterxml.jackson.databind.ObjectMapper; import com.openai.models.chat.completions.ChatCompletion; import com.openai.models.chat.completions.ChatCompletionChunk; import com.openai.models.chat.completions.ChatCompletionCreateParams; -import com.openai.models.chat.completions.ChatCompletionMessage; import com.openai.models.completions.CompletionUsage; -import io.opentelemetry.api.logs.Logger; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; @@ -23,15 +20,11 @@ import lombok.SneakyThrows; final class StreamListener { - private static final ObjectMapper JSON_MAPPER = - new com.fasterxml.jackson.databind.ObjectMapper(); - private final Context context; private final ChatCompletionCreateParams request; private final List choiceBuffers; private final Instrumenter instrumenter; - private final Logger eventLogger; private final boolean captureMessageContent; private final boolean newSpan; private final AtomicBoolean hasEnded; @@ -44,13 +37,11 @@ final class StreamListener { Context context, ChatCompletionCreateParams request, Instrumenter instrumenter, - Logger eventLogger, boolean captureMessageContent, boolean newSpan) { this.context = context; this.request = request; this.instrumenter = instrumenter; - this.eventLogger = eventLogger; this.captureMessageContent = captureMessageContent; this.newSpan = newSpan; choiceBuffers = new ArrayList<>(); @@ -75,19 +66,8 @@ void onChunk(ChatCompletionChunk chunk) { buffer.append(choice.delta()); if (choice.finishReason().isPresent()) { buffer.finishReason = choice.finishReason().get().toString(); - Span.fromContext(context) - .setAttribute( - "braintrust.output_json", - JSON_MAPPER.writeValueAsString( - new ChatCompletionMessage[] {buffer.toChoice().message()})); - - // message has ended, let's emit - ChatCompletionEventsHelper.emitCompletionLogEvent( - context, - eventLogger, - choice.index(), - buffer.finishReason, - buffer.toEventBody()); + BraintrustOAISpanAttributes.setBraintrustOutputJson( + Span.fromContext(context), buffer.toChoice().message()); } } } diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/StreamedMessageBuffer.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/StreamedMessageBuffer.java index 1c1be600..f805611e 100644 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/StreamedMessageBuffer.java +++ b/src/main/java/dev/braintrust/instrumentation/openai/otel/StreamedMessageBuffer.java @@ -9,12 +9,9 @@ import com.openai.models.chat.completions.ChatCompletion; import com.openai.models.chat.completions.ChatCompletionChunk; import com.openai.models.chat.completions.ChatCompletionMessage; -import io.opentelemetry.api.common.Value; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; import javax.annotation.Nullable; final class StreamedMessageBuffer { @@ -52,21 +49,6 @@ ChatCompletion.Choice toChoice() { return choice.build(); } - Value toEventBody() { - Map> body = new HashMap<>(); - if (message != null) { - body.put("content", Value.of(message.toString())); - } - if (toolCalls != null) { - List> toolCallsJson = - toolCalls.values().stream() - .map(StreamedMessageBuffer::buildToolCallEventObject) - .collect(Collectors.toList()); - body.put("tool_calls", Value.of(toolCallsJson)); - } - return Value.of(body); - } - void append(ChatCompletionChunk.Choice.Delta delta) { if (captureMessageContent) { if (delta.content().isPresent()) { @@ -108,25 +90,6 @@ void append(ChatCompletionChunk.Choice.Delta delta) { } } - private static Value buildToolCallEventObject(ToolCallBuffer call) { - Map> result = new HashMap<>(); - result.put("id", Value.of(call.id)); - if (call.type != null) { - result.put("type", Value.of(call.type)); - } - - Map> function = new HashMap<>(); - if (call.function.name != null) { - function.put("name", Value.of(call.function.name)); - } - if (call.function.arguments != null) { - function.put("arguments", Value.of(call.function.arguments.toString())); - } - result.put("function", Value.of(function)); - - return Value.of(result); - } - private static class FunctionBuffer { @Nullable String name; @Nullable StringBuilder arguments;