diff --git a/langsmith-java-core/src/main/java/com/langchain/smith/otel/OtelSpanCreator.java b/langsmith-java-core/src/main/java/com/langchain/smith/otel/OtelSpanCreator.java index d3caf1a8..6baba005 100644 --- a/langsmith-java-core/src/main/java/com/langchain/smith/otel/OtelSpanCreator.java +++ b/langsmith-java-core/src/main/java/com/langchain/smith/otel/OtelSpanCreator.java @@ -1,18 +1,14 @@ package com.langchain.smith.otel; -import com.langchain.smith.models.runs.Run; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.SpanBuilder; -import io.opentelemetry.api.trace.SpanContext; -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.api.trace.StatusCode; -import io.opentelemetry.api.trace.TraceFlags; -import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; -import java.time.Instant; +/** + * Utility class for creating OpenTelemetry spans with Gen AI semantic conventions. + * Provides helper methods to create spans for LLM, tool, retrieval, and chain operations. + */ public final class OtelSpanCreator { private OtelSpanCreator() {} @@ -144,166 +140,4 @@ public final class OtelSpanCreator { } return span; } - - /** - * Creates and exports a span from a LangSmith Run using the native OTEL API. - * - * @param tracer the OpenTelemetry tracer - * @param run the LangSmith Run to convert - * @param projectName the project name for langsmith.project.name attribute - */ - public static void createSpanFromRun(Tracer tracer, Run run, String projectName) { - // Parse timestamps - Instant startTime = parseTime(run.startTime().orElse(null)); - Instant endTime = parseTime(run.endTime().orElse(null)); - - if (startTime == null) { - startTime = Instant.now().minusSeconds(1); - } - if (endTime == null) { - endTime = Instant.now(); - } - - String traceIdHex = normalizeTraceId(run.traceId() - .orElseGet(() -> java.util.UUID.randomUUID().toString().replace("-", ""))); - String spanIdHex = normalizeSpanId(run.id() - .orElseGet(() -> - java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 16))); - String parentSpanIdHex = - run.parentRunId().map(OtelSpanCreator::normalizeSpanId).orElse(null); - - // Create SpanContext for this span - SpanContext spanContext = - SpanContext.create(traceIdHex, spanIdHex, TraceFlags.getSampled(), TraceState.getDefault()); - - // Create parent context if we have a parent span ID - Context parentContext = Context.root(); - if (parentSpanIdHex != null) { - SpanContext parentSpanContext = - SpanContext.create(traceIdHex, parentSpanIdHex, TraceFlags.getSampled(), TraceState.getDefault()); - parentContext = Context.root().with(Span.wrap(parentSpanContext)); - } - - SpanKind spanKind = determineSpanKind(run); - - String spanName = run.name().orElse("langsmith.run"); - SpanBuilder spanBuilder = tracer.spanBuilder(spanName) - .setSpanKind(spanKind) - .setParent(parentContext) - .setStartTimestamp(startTime); - - Span span = spanBuilder.startSpan(); - - try { - // Set OpenTelemetry GenAI semantic convention attributes - - span.setAttribute( - AttributeKey.stringKey("gen_ai.operation.name"), - mapRunTypeToOperation(run.runType().orElse(null))); - - run.name().ifPresent(name -> { - if (name.contains("openai") || name.contains("gpt")) { - span.setAttribute(AttributeKey.stringKey("gen_ai.system"), "openai"); - } else if (name.contains("anthropic") || name.contains("claude")) { - span.setAttribute(AttributeKey.stringKey("gen_ai.system"), "anthropic"); - } - }); - - if (projectName != null) { - span.setAttribute(AttributeKey.stringKey("service.name"), projectName); - } - run.sessionId().ifPresent(id -> span.setAttribute(AttributeKey.stringKey("session.id"), id)); - - if (run.error().isPresent()) { - span.setStatus(StatusCode.ERROR, run.error().get()); - span.setAttribute(AttributeKey.booleanKey("error"), true); - span.setAttribute(AttributeKey.stringKey("error.type"), "runtime_error"); - } else { - span.setStatus(StatusCode.OK); - } - - run.tags().ifPresent(tags -> { - for (int i = 0; i < tags.size(); i++) { - span.setAttribute(AttributeKey.stringKey("tag." + i), tags.get(i)); - } - }); - - } finally { - span.end(endTime); - } - } - - private static Instant parseTime(String timeString) { - if (timeString == null || timeString.isEmpty()) { - return null; - } - try { - return Instant.parse(timeString); - } catch (Exception e) { - try { - long epochMilli = Long.parseLong(timeString); - return Instant.ofEpochMilli(epochMilli); - } catch (Exception e2) { - return null; - } - } - } - - private static SpanKind determineSpanKind(Run run) { - return run.runType() - .map(rt -> { - if (rt == Run.RunType.LLM) return SpanKind.CLIENT; - if (rt == Run.RunType.CHAIN) return SpanKind.INTERNAL; - if (rt == Run.RunType.TOOL) return SpanKind.CLIENT; - return SpanKind.INTERNAL; - }) - .orElse(SpanKind.INTERNAL); - } - - private static String mapRunTypeToOperation(Run.RunType runType) { - if (runType == null) { - return "chat"; - } - if (runType == Run.RunType.LLM) { - return "chat"; - } else if (runType == Run.RunType.CHAIN) { - return "chat"; - } else if (runType == Run.RunType.TOOL) { - return "tool"; - } else if (runType == Run.RunType.RETRIEVER) { - return "retrieval"; - } else if (runType == Run.RunType.PARSER) { - return "parse"; - } else if (runType == Run.RunType.PROMPT) { - return "prompt"; - } else { - return "chat"; - } - } - - private static String normalizeTraceId(String traceId) { - if (traceId == null) { - traceId = java.util.UUID.randomUUID().toString().replace("-", ""); - } - traceId = traceId.toLowerCase(); - if (traceId.length() < 32) { - traceId = String.format("%32s", traceId).replace(' ', '0'); - } else if (traceId.length() > 32) { - traceId = traceId.substring(0, 32); - } - return traceId; - } - - private static String normalizeSpanId(String spanId) { - if (spanId == null) { - spanId = java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 16); - } - spanId = spanId.toLowerCase(); - if (spanId.length() < 16) { - spanId = String.format("%16s", spanId).replace(' ', '0'); - } else if (spanId.length() > 16) { - spanId = spanId.substring(0, 16); - } - return spanId; - } } diff --git a/langsmith-java-core/src/main/java/com/langchain/smith/otel/OtelTraceExporter.java b/langsmith-java-core/src/main/java/com/langchain/smith/otel/OtelTraceExporter.java index cb7511a7..87d72512 100644 --- a/langsmith-java-core/src/main/java/com/langchain/smith/otel/OtelTraceExporter.java +++ b/langsmith-java-core/src/main/java/com/langchain/smith/otel/OtelTraceExporter.java @@ -1,6 +1,5 @@ package com.langchain.smith.otel; -import com.langchain.smith.models.runs.Run; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; @@ -20,10 +19,10 @@ import java.util.logging.Level; import java.util.logging.Logger; /** - * Exports LangSmith runs as OpenTelemetry traces to an OTEL endpoint. + * Manages OpenTelemetry SDK for exporting traces to OTLP endpoints. * - *

This exporter converts LangSmith Run objects to OpenTelemetry spans and sends them to a - * configured OTEL endpoint using the OTLP HTTP protocol. + *

This class initializes the OpenTelemetry SDK with OTLP HTTP export capabilities, + * providing a Tracer for creating spans with Gen AI semantic conventions. */ public final class OtelTraceExporter { private static final Logger logger = Logger.getLogger(OtelTraceExporter.class.getName()); @@ -72,55 +71,6 @@ public final class OtelTraceExporter { return projectName; } - /** - * Exports a single run as a span. - * - * @param run the run to export - * @return a CompletableResultCode indicating success or failure - */ - public CompletableResultCode export(Run run) { - return export(Collections.singletonList(run)); - } - - /** - * Exports multiple runs as spans. - * - * @param runs the runs to export - * @return a CompletableResultCode indicating success or failure - */ - public CompletableResultCode export(List runs) { - if (!config.isEnabled() || runs == null || runs.isEmpty()) { - return CompletableResultCode.ofSuccess(); - } - - try { - logger.log(Level.FINE, "Creating {0} spans using native OTEL API", runs.size()); - - for (Run run : runs) { - OtelSpanCreator.createSpanFromRun(tracer, run, projectName); - } - - // Force flush to ensure spans are exported immediately - CompletableResultCode result = tracerProvider.forceFlush(); - - result.whenComplete(() -> { - if (!result.isSuccess()) { - logger.log( - Level.SEVERE, "Export failed! Check OpenTelemetry logs above for HTTP response details."); - logger.log(Level.SEVERE, "Failed to export {0} spans to {1}", new Object[] { - runs.size(), config.getEndpoint() - }); - } - }); - - return result; - } catch (Exception e) { - logger.log(Level.SEVERE, "Exception while exporting runs to OTEL", e); - e.printStackTrace(); - return CompletableResultCode.ofFailure(); - } - } - /** * Shuts down the exporter and flushes any pending exports. * diff --git a/langsmith-java-core/src/main/kotlin/com/langchain/smith/models/datasets/DatasetListParams.kt b/langsmith-java-core/src/main/kotlin/com/langchain/smith/models/datasets/DatasetListParams.kt index c5645433..c2839eb1 100644 --- a/langsmith-java-core/src/main/kotlin/com/langchain/smith/models/datasets/DatasetListParams.kt +++ b/langsmith-java-core/src/main/kotlin/com/langchain/smith/models/datasets/DatasetListParams.kt @@ -354,7 +354,9 @@ private constructor( put("data_type", dataTypes.joinToString(",") { it.toString() }) } - override fun visitDataType(dataType: com.langchain.smith.models.datasets.DataType) { + override fun visitDataType( + dataType: com.langchain.smith.models.datasets.DataType + ) { put("data_type", dataType.toString()) } } @@ -384,7 +386,8 @@ private constructor( fun dataTypes(): Optional> = Optional.ofNullable(dataTypes) /** Enum for dataset data types. */ - fun dataType(): Optional = Optional.ofNullable(dataType) + fun dataType(): Optional = + Optional.ofNullable(dataType) fun isDataTypes(): Boolean = dataTypes != null @@ -393,7 +396,8 @@ private constructor( fun asDataTypes(): List = dataTypes.getOrThrow("dataTypes") /** Enum for dataset data types. */ - fun asDataType(): com.langchain.smith.models.datasets.DataType = dataType.getOrThrow("dataType") + fun asDataType(): com.langchain.smith.models.datasets.DataType = + dataType.getOrThrow("dataType") fun accept(visitor: Visitor): T = when {