diff --git a/pom.xml b/pom.xml
index 69504a5d..5b41b47d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -65,6 +65,7 @@
trasier-client-spring-grpc
trasier-client-spring-grpc-starter
trasier-client-opentracing
+ trasier-client-opentelemetry
trasier-client-spring-interceptor
trasier-client-spring-interceptor-ws
trasier-client-spring-boot
diff --git a/trasier-client-api-protobuf/src/main/java/com/trasier/api/client/protobuf/WriteServiceGrpc.java b/trasier-client-api-protobuf/src/main/java/com/trasier/api/client/protobuf/WriteServiceGrpc.java
index bc342083..dcfff615 100644
--- a/trasier-client-api-protobuf/src/main/java/com/trasier/api/client/protobuf/WriteServiceGrpc.java
+++ b/trasier-client-api-protobuf/src/main/java/com/trasier/api/client/protobuf/WriteServiceGrpc.java
@@ -5,11 +5,6 @@
import static io.grpc.stub.ServerCalls.asyncBidiStreamingCall;
import static io.grpc.stub.ServerCalls.asyncUnimplementedStreamingCall;
-/**
- */
-@javax.annotation.Generated(
- value = "by gRPC proto compiler (version 1.24.0)",
- comments = "Source: WriteService.proto")
public final class WriteServiceGrpc {
private WriteServiceGrpc() {}
diff --git a/trasier-client-opentelemetry/pom.xml b/trasier-client-opentelemetry/pom.xml
new file mode 100644
index 00000000..3a74e6a7
--- /dev/null
+++ b/trasier-client-opentelemetry/pom.xml
@@ -0,0 +1,41 @@
+
+
+
+ trasier-client
+ com.trasier
+ 2.3.9-SNAPSHOT
+
+ 4.0.0
+
+ trasier-client-opentelemetry
+ jar
+
+
+ 11
+ 11
+ 4.12
+ 0.16.0
+
+
+
+
+ com.trasier
+ trasier-client-core
+
+
+ io.opentelemetry
+ opentelemetry-sdk
+ ${opentelemetry.version}
+
+
+
+ junit
+ junit
+ ${junit.version}
+ test
+
+
+
+
\ No newline at end of file
diff --git a/trasier-client-opentelemetry/src/main/java/com/trasier/client/opentelemetry/TrasierSpanExporter.java b/trasier-client-opentelemetry/src/main/java/com/trasier/client/opentelemetry/TrasierSpanExporter.java
new file mode 100644
index 00000000..65068903
--- /dev/null
+++ b/trasier-client-opentelemetry/src/main/java/com/trasier/client/opentelemetry/TrasierSpanExporter.java
@@ -0,0 +1,166 @@
+package com.trasier.client.opentelemetry;
+
+import com.trasier.client.api.Client;
+import com.trasier.client.api.Endpoint;
+import com.trasier.client.api.Span;
+import com.trasier.client.configuration.TrasierClientConfiguration;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.sdk.common.CompletableResultCode;
+import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.opentelemetry.sdk.trace.export.SpanExporter;
+import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+import java.util.logging.Logger;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+public final class TrasierSpanExporter implements SpanExporter {
+
+ private static final Logger logger = Logger.getLogger(TrasierSpanExporter.class.getName());
+
+ private Client client;
+ private TrasierClientConfiguration configuration;
+
+ public TrasierSpanExporter(Client client, TrasierClientConfiguration configuration) {
+ this.client = client;
+ this.configuration = configuration;
+ }
+
+ @Override
+ public CompletableResultCode export(final Collection spanDataList) {
+ List spans = new ArrayList<>(spanDataList.size());
+ for (SpanData spanData : spanDataList) {
+ spans.add(convertSpan(spanData));
+ }
+
+ final CompletableResultCode result = new CompletableResultCode();
+
+ client.sendSpans(spans);
+
+ return result;
+ }
+
+ @Override
+ public CompletableResultCode flush() {
+ return CompletableResultCode.ofSuccess();
+ }
+
+ @Override
+ public CompletableResultCode shutdown() {
+ client.close();
+ return CompletableResultCode.ofSuccess();
+ }
+
+ static final String OTEL_STATUS_CODE = "otel.status_code";
+
+ static final String KEY_INSTRUMENTATION_LIBRARY_NAME = "otel.library.name";
+ static final String KEY_INSTRUMENTATION_LIBRARY_VERSION = "otel.library.version";
+
+ Span convertSpan(SpanData spanData) {
+ Endpoint endpoint = getEndpoint(spanData);
+
+ long startTimestamp = toEpochMicros(spanData.getStartEpochNanos());
+ long endTimestamp = toEpochMicros(spanData.getEndEpochNanos());
+
+ final Span.SpanBuilder spanBuilder =
+ Span.newSpan(spanData.getName(), UUID.randomUUID().toString(), spanData.getTraceId(), spanData.getSpanId())
+ //.kind(toSpanKind(spanData))
+ .startTimestamp(startTimestamp)
+ .endTimestamp(endTimestamp);
+
+ if (spanData.getKind() == SpanKind.CLIENT || spanData.getKind() == SpanKind.PRODUCER) {
+ spanBuilder.incomingEndpoint(endpoint);
+ }
+
+ if (spanData.getKind() == SpanKind.SERVER || spanData.getKind() == SpanKind.CONSUMER) {
+ spanBuilder.outgoingEndpoint(endpoint);
+ }
+
+ if (spanData.getParentSpanContext().isValid()) {
+ spanBuilder.parentId(spanData.getParentSpanId());
+ }
+/*
+ Attributes spanAttributes = spanData.getAttributes();
+ spanAttributes.forEach(
+ (key, value) -> spanBuilder.putTag(key.getKey(), valueToString(key, value)));
+ StatusData status = spanData.getStatus();
+
+ // include status code & error.
+ if (status.getStatusCode() != StatusCode.UNSET) {
+ spanBuilder.putTag(OTEL_STATUS_CODE, status.getStatusCode().toString());
+
+ // add the error tag, if it isn't already in the source span.
+ if (status.getStatusCode() == StatusCode.ERROR && spanAttributes.get(STATUS_ERROR) == null) {
+ spanBuilder.putTag(STATUS_ERROR.getKey(), nullToEmpty(status.getDescription()));
+ }
+ }
+
+ InstrumentationLibraryInfo instrumentationLibraryInfo =
+ spanData.getInstrumentationLibraryInfo();
+
+ if (!instrumentationLibraryInfo.getName().isEmpty()) {
+ spanBuilder.putTag(KEY_INSTRUMENTATION_LIBRARY_NAME, instrumentationLibraryInfo.getName());
+ }
+ if (instrumentationLibraryInfo.getVersion() != null) {
+ spanBuilder.putTag(
+ KEY_INSTRUMENTATION_LIBRARY_VERSION, instrumentationLibraryInfo.getVersion());
+ }
+
+ for (EventData annotation : spanData.getEvents()) {
+ spanBuilder.addAnnotation(toEpochMicros(annotation.getEpochNanos()), annotation.getName());
+ }
+*/
+ return spanBuilder.build();
+ }
+
+ private Endpoint getEndpoint(SpanData spanData) {
+ Attributes resourceAttributes = spanData.getResource().getAttributes();
+
+ // use the service.name from the Resource, if it's been set.
+ String serviceNameValue = resourceAttributes.get(ResourceAttributes.SERVICE_NAME);
+ if (serviceNameValue == null) {
+ serviceNameValue = Resource.getDefault().getAttributes().get(ResourceAttributes.SERVICE_NAME);
+ }
+ return new Endpoint(serviceNameValue);
+ }
+
+
+ private static long toEpochMicros(long epochNanos) {
+ return NANOSECONDS.toMicros(epochNanos);
+ }
+/*
+ private static String valueToString(AttributeKey> key, Object attributeValue) {
+ AttributeType type = key.getType();
+ switch (type) {
+ case STRING:
+ case BOOLEAN:
+ case LONG:
+ case DOUBLE:
+ return String.valueOf(attributeValue);
+ case STRING_ARRAY:
+ case BOOLEAN_ARRAY:
+ case LONG_ARRAY:
+ case DOUBLE_ARRAY:
+ return commaSeparated((List>) attributeValue);
+ }
+ throw new IllegalStateException("Unknown attribute type: " + type);
+ }
+
+ private static String commaSeparated(List> values) {
+ StringBuilder builder = new StringBuilder();
+ for (Object value : values) {
+ if (builder.length() != 0) {
+ builder.append(',');
+ }
+ builder.append(value);
+ }
+ return builder.toString();
+ }
+*/
+}
\ No newline at end of file