diff --git a/exporters/otlp/build.gradle b/exporters/otlp/build.gradle index e0fe075609..0e6239fcec 100644 --- a/exporters/otlp/build.gradle +++ b/exporters/otlp/build.gradle @@ -9,10 +9,12 @@ description = 'OpenTelemetry Protocol Exporter' ext.moduleName = "io.opentelemetry.exporters.otprotocol" dependencies { - api project(':opentelemetry-proto'), - project(':opentelemetry-sdk') + api project(':opentelemetry-sdk') - implementation libraries.grpc_protobuf, + implementation project(':opentelemetry-sdk-contrib-otproto'), + project(':opentelemetry-sdk'), + libraries.grpc_api, + libraries.grpc_protobuf, libraries.grpc_stub, libraries.protobuf, libraries.protobuf_util diff --git a/exporters/otlp/src/main/java/io/opentelemetry/exporters/otlp/CommonAdapter.java b/exporters/otlp/src/main/java/io/opentelemetry/exporters/otlp/CommonAdapter.java new file mode 100644 index 0000000000..dcaedd3f68 --- /dev/null +++ b/exporters/otlp/src/main/java/io/opentelemetry/exporters/otlp/CommonAdapter.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.exporters.otlp; + +import io.opentelemetry.proto.common.v1.AttributeKeyValue; +import io.opentelemetry.proto.common.v1.AttributeKeyValue.ValueType; +import io.opentelemetry.trace.AttributeValue; + +final class CommonAdapter { + static AttributeKeyValue toProtoAttribute(String key, AttributeValue attributeValue) { + AttributeKeyValue.Builder builder = AttributeKeyValue.newBuilder().setKey(key); + switch (attributeValue.getType()) { + case STRING: + return builder + .setType(ValueType.STRING) + .setStringValue(attributeValue.getStringValue()) + .build(); + case BOOLEAN: + return builder + .setType(ValueType.BOOL) + .setBoolValue(attributeValue.getBooleanValue()) + .build(); + case LONG: + return builder.setType(ValueType.INT).setIntValue(attributeValue.getLongValue()).build(); + case DOUBLE: + return builder + .setType(ValueType.DOUBLE) + .setDoubleValue(attributeValue.getDoubleValue()) + .build(); + } + return builder.setType(ValueType.UNRECOGNIZED).build(); + } + + private CommonAdapter() {} +} diff --git a/exporters/otlp/src/main/java/io/opentelemetry/exporters/otlp/OtlpGrpcSpanExporter.java b/exporters/otlp/src/main/java/io/opentelemetry/exporters/otlp/OtlpGrpcSpanExporter.java new file mode 100644 index 0000000000..2ddfc16b01 --- /dev/null +++ b/exporters/otlp/src/main/java/io/opentelemetry/exporters/otlp/OtlpGrpcSpanExporter.java @@ -0,0 +1,153 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.exporters.otlp; + +import io.grpc.ManagedChannel; +import io.grpc.StatusRuntimeException; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.concurrent.ThreadSafe; + +/** Exports spans using OTLP via gRPC, using OpenTelemetry's protobuf model. */ +@ThreadSafe +public final class OtlpGrpcSpanExporter implements SpanExporter { + private static final Logger logger = Logger.getLogger(OtlpGrpcSpanExporter.class.getName()); + + private final TraceServiceGrpc.TraceServiceBlockingStub blockingStub; + private final ManagedChannel managedChannel; + private final long deadlineMs; + + /** + * Creates a new Jaeger gRPC Span Reporter with the given name, using the given channel. + * + * @param channel the channel to use when communicating with the Jaeger Collector. + * @param deadlineMs max waiting time for the collector to process each span batch. When set to 0 + * or to a negative value, the exporter will wait indefinitely. + */ + private OtlpGrpcSpanExporter(ManagedChannel channel, long deadlineMs) { + this.managedChannel = channel; + this.blockingStub = TraceServiceGrpc.newBlockingStub(channel); + this.deadlineMs = deadlineMs; + } + + /** + * Submits all the given spans in a single batch to the Jaeger collector. + * + * @param spans the list of sampled Spans to be exported. + * @return the result of the operation + */ + @Override + public ResultCode export(List spans) { + ExportTraceServiceRequest exportTraceServiceRequest = + ExportTraceServiceRequest.newBuilder() + .addAllResourceSpans(SpanAdapter.toProtoResourceSpans(spans)) + .build(); + + try { + TraceServiceGrpc.TraceServiceBlockingStub stub = this.blockingStub; + if (deadlineMs > 0) { + stub = stub.withDeadlineAfter(deadlineMs, TimeUnit.MILLISECONDS); + } + + // for now, there's nothing to check in the response object + // noinspection ResultOfMethodCallIgnored + stub.export(exportTraceServiceRequest); + return ResultCode.SUCCESS; + } catch (StatusRuntimeException e) { + // Retryable codes from https://github.com/open-telemetry/oteps/pull/65 + switch (e.getStatus().getCode()) { + case CANCELLED: + case DEADLINE_EXCEEDED: + case RESOURCE_EXHAUSTED: + case OUT_OF_RANGE: + case UNAVAILABLE: + case DATA_LOSS: + logger.info(e.getStatus().toString()); + return ResultCode.FAILED_RETRYABLE; + default: + return ResultCode.FAILED_NOT_RETRYABLE; + } + } catch (Throwable t) { + return ResultCode.FAILED_NOT_RETRYABLE; + } + } + + /** + * Creates a new builder instance. + * + * @return a new instance builder for this exporter + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new calls are immediately + * cancelled. The channel is forcefully closed after a timeout. + */ + @Override + public void shutdown() { + try { + managedChannel.shutdown().awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + logger.log(Level.WARNING, "Failed to shutdown the gRPC channel", e); + } + } + + /** Builder utility for this exporter. */ + public static class Builder { + private ManagedChannel channel; + private long deadlineMs = 1_000; // 1 second + + /** + * Sets the managed chanel to use when communicating with the backend. Required. + * + * @param channel the channel to use + * @return this builder's instance + */ + public Builder setChannel(ManagedChannel channel) { + this.channel = channel; + return this; + } + + /** + * Sets the max waiting time for the collector to process each span batch. Optional. + * + * @param deadlineMs the max waiting time + * @return this builder's instance + */ + public Builder setDeadlineMs(long deadlineMs) { + this.deadlineMs = deadlineMs; + return this; + } + + /** + * Constructs a new instance of the exporter based on the builder's values. + * + * @return a new exporter's instance + */ + public OtlpGrpcSpanExporter build() { + return new OtlpGrpcSpanExporter(channel, deadlineMs); + } + } +} diff --git a/exporters/otlp/src/main/java/io/opentelemetry/exporters/otlp/ResourceAdapter.java b/exporters/otlp/src/main/java/io/opentelemetry/exporters/otlp/ResourceAdapter.java new file mode 100644 index 0000000000..099367ae9d --- /dev/null +++ b/exporters/otlp/src/main/java/io/opentelemetry/exporters/otlp/ResourceAdapter.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.exporters.otlp; + +import io.opentelemetry.proto.resource.v1.Resource; +import io.opentelemetry.trace.AttributeValue; +import java.util.Map; + +final class ResourceAdapter { + static Resource toProtoResource(io.opentelemetry.sdk.resources.Resource resource) { + Resource.Builder builder = Resource.newBuilder(); + for (Map.Entry resourceEntry : resource.getLabels().entrySet()) { + builder.addAttributes( + CommonAdapter.toProtoAttribute(resourceEntry.getKey(), resourceEntry.getValue())); + } + return builder.build(); + } + + private ResourceAdapter() {} +} diff --git a/exporters/otlp/src/main/java/io/opentelemetry/exporters/otlp/SpanAdapter.java b/exporters/otlp/src/main/java/io/opentelemetry/exporters/otlp/SpanAdapter.java new file mode 100644 index 0000000000..ddb7b601a6 --- /dev/null +++ b/exporters/otlp/src/main/java/io/opentelemetry/exporters/otlp/SpanAdapter.java @@ -0,0 +1,135 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.exporters.otlp; + +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import io.opentelemetry.proto.trace.v1.Span; +import io.opentelemetry.proto.trace.v1.Span.SpanKind; +import io.opentelemetry.proto.trace.v1.Status; +import io.opentelemetry.proto.trace.v1.Status.StatusCode; +import io.opentelemetry.sdk.contrib.otproto.TraceProtoUtils; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.SpanData.TimedEvent; +import io.opentelemetry.trace.AttributeValue; +import io.opentelemetry.trace.Link; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +final class SpanAdapter { + static List toProtoResourceSpans(List spanDataList) { + Map resourceSpansBuilderMap = new HashMap<>(); + for (SpanData spanData : spanDataList) { + Resource resource = spanData.getResource(); + ResourceSpans.Builder resourceSpansBuilder = + resourceSpansBuilderMap.get(spanData.getResource()); + if (resourceSpansBuilder == null) { + resourceSpansBuilder = + ResourceSpans.newBuilder().setResource(ResourceAdapter.toProtoResource(resource)); + resourceSpansBuilderMap.put(resource, resourceSpansBuilder); + } + resourceSpansBuilder.addSpans(toProtoSpan(spanData)); + } + List resourceSpans = new ArrayList<>(resourceSpansBuilderMap.size()); + for (ResourceSpans.Builder resourceSpansBuilder : resourceSpansBuilderMap.values()) { + resourceSpans.add(resourceSpansBuilder.build()); + } + return resourceSpans; + } + + static Span toProtoSpan(SpanData spanData) { + Span.Builder builder = Span.newBuilder(); + builder.setTraceId(TraceProtoUtils.toProtoTraceId(spanData.getTraceId())); + builder.setSpanId(TraceProtoUtils.toProtoSpanId(spanData.getSpanId())); + // TODO: Set TraceState; + builder.setParentSpanId(TraceProtoUtils.toProtoSpanId(spanData.getParentSpanId())); + builder.setName(spanData.getName()); + builder.setKind(toProtoSpanKind(spanData.getKind())); + builder.setStartTimeUnixnano(spanData.getStartEpochNanos()); + builder.setEndTimeUnixnano(spanData.getEndEpochNanos()); + for (Map.Entry resourceEntry : spanData.getAttributes().entrySet()) { + builder.addAttributes( + CommonAdapter.toProtoAttribute(resourceEntry.getKey(), resourceEntry.getValue())); + } + // TODO: Set DroppedAttributesCount; + for (TimedEvent timedEvent : spanData.getTimedEvents()) { + builder.addEvents(toProtoSpanEvent(timedEvent)); + } + builder.setDroppedEventsCount( + spanData.getTotalRecordedEvents() - spanData.getTimedEvents().size()); + for (Link link : spanData.getLinks()) { + builder.addLinks(toProtoSpanLink(link)); + } + builder.setDroppedLinksCount(spanData.getTotalRecordedLinks() - spanData.getLinks().size()); + builder.setStatus(toStatusProto(spanData.getStatus())); + return builder.build(); + } + + static Span.SpanKind toProtoSpanKind(io.opentelemetry.trace.Span.Kind kind) { + switch (kind) { + case INTERNAL: + return SpanKind.INTERNAL; + case SERVER: + return SpanKind.SERVER; + case CLIENT: + return SpanKind.CLIENT; + case PRODUCER: + return SpanKind.PRODUCER; + case CONSUMER: + return SpanKind.CONSUMER; + } + return SpanKind.UNRECOGNIZED; + } + + static Span.Event toProtoSpanEvent(TimedEvent timedEvent) { + Span.Event.Builder builder = Span.Event.newBuilder(); + builder.setName(timedEvent.getName()); + builder.setTimeUnixnano(timedEvent.getEpochNanos()); + for (Map.Entry resourceEntry : timedEvent.getAttributes().entrySet()) { + builder.addAttributes( + CommonAdapter.toProtoAttribute(resourceEntry.getKey(), resourceEntry.getValue())); + } + // TODO: Set DroppedAttributesCount; + return builder.build(); + } + + static Span.Link toProtoSpanLink(Link link) { + Span.Link.Builder builder = Span.Link.newBuilder(); + builder.setTraceId(TraceProtoUtils.toProtoTraceId(link.getContext().getTraceId())); + builder.setSpanId(TraceProtoUtils.toProtoSpanId(link.getContext().getSpanId())); + // TODO: Set TraceState; + for (Map.Entry resourceEntry : link.getAttributes().entrySet()) { + builder.addAttributes( + CommonAdapter.toProtoAttribute(resourceEntry.getKey(), resourceEntry.getValue())); + } + // TODO: Set DroppedAttributesCount; + return builder.build(); + } + + static Status toStatusProto(io.opentelemetry.trace.Status status) { + Status.Builder builder = + Status.newBuilder().setCode(StatusCode.forNumber(status.getCanonicalCode().value())); + if (status.getDescription() != null) { + builder.setMessage(status.getDescription()); + } + return builder.build(); + } + + private SpanAdapter() {} +} diff --git a/exporters/otlp/src/test/java/io/opentelemetry/exporters/otlp/CommonAdapterTest.java b/exporters/otlp/src/test/java/io/opentelemetry/exporters/otlp/CommonAdapterTest.java new file mode 100644 index 0000000000..9a6d0bd3c5 --- /dev/null +++ b/exporters/otlp/src/test/java/io/opentelemetry/exporters/otlp/CommonAdapterTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.exporters.otlp; + +import static com.google.common.truth.Truth.assertThat; + +import io.opentelemetry.proto.common.v1.AttributeKeyValue; +import io.opentelemetry.proto.common.v1.AttributeKeyValue.ValueType; +import io.opentelemetry.trace.AttributeValue; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link CommonAdapter}. */ +@RunWith(JUnit4.class) +public class CommonAdapterTest { + @Test + public void toProtoAttribute_Bool() { + assertThat(CommonAdapter.toProtoAttribute("key", AttributeValue.booleanAttributeValue(true))) + .isEqualTo( + AttributeKeyValue.newBuilder() + .setKey("key") + .setBoolValue(true) + .setType(ValueType.BOOL) + .build()); + } + + @Test + public void toProtoAttribute_String() { + assertThat(CommonAdapter.toProtoAttribute("key", AttributeValue.stringAttributeValue("string"))) + .isEqualTo( + AttributeKeyValue.newBuilder() + .setKey("key") + .setStringValue("string") + .setType(ValueType.STRING) + .build()); + } + + @Test + public void toProtoAttribute_Int() { + assertThat(CommonAdapter.toProtoAttribute("key", AttributeValue.longAttributeValue(100))) + .isEqualTo( + AttributeKeyValue.newBuilder() + .setKey("key") + .setIntValue(100) + .setType(ValueType.INT) + .build()); + } + + @Test + public void toProtoAttribute_Double() { + assertThat(CommonAdapter.toProtoAttribute("key", AttributeValue.doubleAttributeValue(100.3))) + .isEqualTo( + AttributeKeyValue.newBuilder() + .setKey("key") + .setDoubleValue(100.3) + .setType(ValueType.DOUBLE) + .build()); + } +} diff --git a/exporters/otlp/src/test/java/io/opentelemetry/exporters/otlp/OtlpGrpcSpanExporterTest.java b/exporters/otlp/src/test/java/io/opentelemetry/exporters/otlp/OtlpGrpcSpanExporterTest.java new file mode 100644 index 0000000000..3198af28f9 --- /dev/null +++ b/exporters/otlp/src/test/java/io/opentelemetry/exporters/otlp/OtlpGrpcSpanExporterTest.java @@ -0,0 +1,253 @@ +/* + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.exporters.otlp; + +import static com.google.common.truth.Truth.assertThat; + +import io.grpc.ManagedChannel; +import io.grpc.Status.Code; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.testing.GrpcCleanupRule; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse; +import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter.ResultCode; +import io.opentelemetry.trace.Link; +import io.opentelemetry.trace.Span.Kind; +import io.opentelemetry.trace.SpanId; +import io.opentelemetry.trace.Status; +import io.opentelemetry.trace.TraceId; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link OtlpGrpcSpanExporter}. */ +@RunWith(JUnit4.class) +public class OtlpGrpcSpanExporterTest { + private static final String TRACE_ID = "00000000000000000000000000abc123"; + private static final String SPAN_ID = "0000000000def456"; + + @Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + + private final FakeCollector fakeCollector = new FakeCollector(); + private final String serverName = InProcessServerBuilder.generateName(); + private final ManagedChannel inProcessChannel = + InProcessChannelBuilder.forName(serverName).directExecutor().build(); + + @Before + public void setup() throws IOException { + grpcCleanup.register( + InProcessServerBuilder.forName(serverName) + .directExecutor() + .addService(fakeCollector) + .build() + .start()); + grpcCleanup.register(inProcessChannel); + } + + @Test + public void testExport() { + SpanData span = generateFakeSpan(); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.newBuilder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(span))).isEqualTo(ResultCode.SUCCESS); + assertThat(fakeCollector.getReceivedSpans()) + .isEqualTo(SpanAdapter.toProtoResourceSpans(Collections.singletonList(span))); + } finally { + exporter.shutdown(); + } + } + + @Test + public void testExport_MultipleSpans() { + List spans = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + spans.add(generateFakeSpan()); + } + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.newBuilder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(spans)).isEqualTo(ResultCode.SUCCESS); + assertThat(fakeCollector.getReceivedSpans()) + .isEqualTo(SpanAdapter.toProtoResourceSpans(spans)); + } finally { + exporter.shutdown(); + } + } + + @Test + public void testExport_AfterShutdown() { + SpanData span = generateFakeSpan(); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.newBuilder().setChannel(inProcessChannel).build(); + exporter.shutdown(); + // TODO: This probably should not be retryable because we never restart the channel. + assertThat(exporter.export(Collections.singletonList(span))) + .isEqualTo(ResultCode.FAILED_RETRYABLE); + } + + @Test + public void testExport_Cancelled() { + fakeCollector.setReturnedStatus(io.grpc.Status.CANCELLED); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.newBuilder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeSpan()))) + .isEqualTo(ResultCode.FAILED_RETRYABLE); + } finally { + exporter.shutdown(); + } + } + + @Test + public void testExport_DeadlineExceeded() { + fakeCollector.setReturnedStatus(io.grpc.Status.DEADLINE_EXCEEDED); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.newBuilder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeSpan()))) + .isEqualTo(ResultCode.FAILED_RETRYABLE); + } finally { + exporter.shutdown(); + } + } + + @Test + public void testExport_ResourceExhausted() { + fakeCollector.setReturnedStatus(io.grpc.Status.RESOURCE_EXHAUSTED); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.newBuilder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeSpan()))) + .isEqualTo(ResultCode.FAILED_RETRYABLE); + } finally { + exporter.shutdown(); + } + } + + @Test + public void testExport_OutOfRange() { + fakeCollector.setReturnedStatus(io.grpc.Status.OUT_OF_RANGE); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.newBuilder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeSpan()))) + .isEqualTo(ResultCode.FAILED_RETRYABLE); + } finally { + exporter.shutdown(); + } + } + + @Test + public void testExport_Unavailable() { + fakeCollector.setReturnedStatus(io.grpc.Status.UNAVAILABLE); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.newBuilder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeSpan()))) + .isEqualTo(ResultCode.FAILED_RETRYABLE); + } finally { + exporter.shutdown(); + } + } + + @Test + public void testExport_DataLoss() { + fakeCollector.setReturnedStatus(io.grpc.Status.DATA_LOSS); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.newBuilder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeSpan()))) + .isEqualTo(ResultCode.FAILED_RETRYABLE); + } finally { + exporter.shutdown(); + } + } + + @Test + public void testExport_PermissionDenied() { + fakeCollector.setReturnedStatus(io.grpc.Status.PERMISSION_DENIED); + OtlpGrpcSpanExporter exporter = + OtlpGrpcSpanExporter.newBuilder().setChannel(inProcessChannel).build(); + try { + assertThat(exporter.export(Collections.singletonList(generateFakeSpan()))) + .isEqualTo(ResultCode.FAILED_NOT_RETRYABLE); + } finally { + exporter.shutdown(); + } + } + + private static SpanData generateFakeSpan() { + long duration = TimeUnit.MILLISECONDS.toNanos(900); + long startNs = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); + long endNs = startNs + duration; + return SpanData.newBuilder() + .setHasEnded(true) + .setTraceId(TraceId.fromLowerBase16(TRACE_ID, 0)) + .setSpanId(SpanId.fromLowerBase16(SPAN_ID, 0)) + .setName("GET /api/endpoint") + .setStartEpochNanos(startNs) + .setEndEpochNanos(endNs) + .setStatus(Status.OK) + .setKind(Kind.SERVER) + .setLinks(Collections.emptyList()) + .setTotalRecordedLinks(0) + .setTotalRecordedEvents(0) + .build(); + } + + private static final class FakeCollector extends TraceServiceGrpc.TraceServiceImplBase { + private final List receivedSpans = new ArrayList<>(); + private io.grpc.Status returnedStatus = io.grpc.Status.OK; + + @Override + public void export( + ExportTraceServiceRequest request, + io.grpc.stub.StreamObserver responseObserver) { + receivedSpans.addAll(request.getResourceSpansList()); + responseObserver.onNext(ExportTraceServiceResponse.newBuilder().build()); + if (!returnedStatus.isOk()) { + if (returnedStatus.getCode() == Code.DEADLINE_EXCEEDED) { + // Do not call onCompleted to simulate a deadline exceeded. + return; + } + responseObserver.onError(returnedStatus.asRuntimeException()); + return; + } + responseObserver.onCompleted(); + } + + List getReceivedSpans() { + return receivedSpans; + } + + void setReturnedStatus(io.grpc.Status returnedStatus) { + this.returnedStatus = returnedStatus; + } + } +} diff --git a/exporters/otlp/src/test/java/io/opentelemetry/exporters/otlp/ResourceAdapterTest.java b/exporters/otlp/src/test/java/io/opentelemetry/exporters/otlp/ResourceAdapterTest.java new file mode 100644 index 0000000000..8448247fec --- /dev/null +++ b/exporters/otlp/src/test/java/io/opentelemetry/exporters/otlp/ResourceAdapterTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.exporters.otlp; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.proto.common.v1.AttributeKeyValue; +import io.opentelemetry.proto.common.v1.AttributeKeyValue.ValueType; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.trace.AttributeValue; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link ResourceAdapter}. */ +@RunWith(JUnit4.class) +public class ResourceAdapterTest { + @Test + public void toProtoResource() { + assertThat( + ResourceAdapter.toProtoResource( + Resource.create( + ImmutableMap.of( + "key_bool", + AttributeValue.booleanAttributeValue(true), + "key_string", + AttributeValue.stringAttributeValue("string"), + "key_int", + AttributeValue.longAttributeValue(100), + "key_double", + AttributeValue.doubleAttributeValue(100.3)))) + .getAttributesList()) + .containsExactly( + AttributeKeyValue.newBuilder() + .setKey("key_bool") + .setBoolValue(true) + .setType(ValueType.BOOL) + .build(), + AttributeKeyValue.newBuilder() + .setKey("key_string") + .setStringValue("string") + .setType(ValueType.STRING) + .build(), + AttributeKeyValue.newBuilder() + .setKey("key_int") + .setIntValue(100) + .setType(ValueType.INT) + .build(), + AttributeKeyValue.newBuilder() + .setKey("key_double") + .setDoubleValue(100.3) + .setType(ValueType.DOUBLE) + .build()); + } + + @Test + public void toProtoResource_Empty() { + assertThat(ResourceAdapter.toProtoResource(Resource.getEmpty())) + .isEqualTo(io.opentelemetry.proto.resource.v1.Resource.newBuilder().build()); + } +} diff --git a/exporters/otlp/src/test/java/io/opentelemetry/exporters/otlp/SpanAdapterTest.java b/exporters/otlp/src/test/java/io/opentelemetry/exporters/otlp/SpanAdapterTest.java new file mode 100644 index 0000000000..817ee959db --- /dev/null +++ b/exporters/otlp/src/test/java/io/opentelemetry/exporters/otlp/SpanAdapterTest.java @@ -0,0 +1,302 @@ +/* + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.exporters.otlp; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.protobuf.ByteString; +import io.opentelemetry.proto.common.v1.AttributeKeyValue; +import io.opentelemetry.proto.common.v1.AttributeKeyValue.ValueType; +import io.opentelemetry.proto.trace.v1.Span; +import io.opentelemetry.proto.trace.v1.Span.SpanKind; +import io.opentelemetry.proto.trace.v1.Status; +import io.opentelemetry.proto.trace.v1.Status.StatusCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.SpanData.Link; +import io.opentelemetry.sdk.trace.data.SpanData.TimedEvent; +import io.opentelemetry.trace.AttributeValue; +import io.opentelemetry.trace.Span.Kind; +import io.opentelemetry.trace.SpanContext; +import io.opentelemetry.trace.SpanId; +import io.opentelemetry.trace.TraceFlags; +import io.opentelemetry.trace.TraceId; +import io.opentelemetry.trace.TraceState; +import java.util.Collections; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link ResourceAdapter}. */ +@RunWith(JUnit4.class) +public class SpanAdapterTest { + private static final byte[] TRACE_ID_BYTES = + new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}; + private static final TraceId TRACE_ID = TraceId.fromBytes(TRACE_ID_BYTES, 0); + private static final byte[] SPAN_ID_BYTES = new byte[] {0, 0, 0, 0, 4, 3, 2, 1}; + private static final SpanId SPAN_ID = SpanId.fromBytes(SPAN_ID_BYTES, 0); + private static final TraceState TRACE_STATE = TraceState.builder().build(); + private static final SpanContext SPAN_CONTEXT = + SpanContext.create( + TRACE_ID, SPAN_ID, TraceFlags.builder().setIsSampled(true).build(), TRACE_STATE); + + @Test + public void toProtoSpan() { + Span span = + SpanAdapter.toProtoSpan( + SpanData.newBuilder() + .setHasEnded(true) + .setTraceId(TRACE_ID) + .setSpanId(SPAN_ID) + .setParentSpanId(SpanId.getInvalid()) + .setName("GET /api/endpoint") + .setKind(Kind.SERVER) + .setStartEpochNanos(12345) + .setEndEpochNanos(12349) + .setAttributes( + Collections.singletonMap("key", AttributeValue.booleanAttributeValue(true))) + .setTimedEvents( + Collections.singletonList( + TimedEvent.create( + 12347, "my_event", Collections.emptyMap()))) + .setTotalRecordedEvents(3) + .setLinks( + Collections.singletonList( + Link.create(SPAN_CONTEXT))) + .setTotalRecordedLinks(2) + .setStatus(io.opentelemetry.trace.Status.OK) + .build()); + + assertThat(span.getTraceId().toByteArray()).isEqualTo(TRACE_ID_BYTES); + assertThat(span.getSpanId().toByteArray()).isEqualTo(SPAN_ID_BYTES); + assertThat(span.getParentSpanId().toByteArray()).isEqualTo(new byte[] {0, 0, 0, 0, 0, 0, 0, 0}); + assertThat(span.getName()).isEqualTo("GET /api/endpoint"); + assertThat(span.getKind()).isEqualTo(SpanKind.SERVER); + assertThat(span.getStartTimeUnixnano()).isEqualTo(12345); + assertThat(span.getEndTimeUnixnano()).isEqualTo(12349); + assertThat(span.getAttributesList()) + .containsExactly( + AttributeKeyValue.newBuilder() + .setKey("key") + .setBoolValue(true) + .setType(ValueType.BOOL) + .build()); + // Fix this when we plugged dropped attributes. + assertThat(span.getDroppedAttributesCount()).isEqualTo(0); + assertThat(span.getEventsList()) + .containsExactly( + Span.Event.newBuilder().setTimeUnixnano(12347).setName("my_event").build()); + assertThat(span.getDroppedEventsCount()).isEqualTo(2); // 3 - 1 + assertThat(span.getLinksList()) + .containsExactly( + Span.Link.newBuilder() + .setTraceId(ByteString.copyFrom(TRACE_ID_BYTES)) + .setSpanId(ByteString.copyFrom(SPAN_ID_BYTES)) + .build()); + assertThat(span.getDroppedLinksCount()).isEqualTo(1); // 2 - 1 + assertThat(span.getStatus()).isEqualTo(Status.newBuilder().setCode(StatusCode.Ok).build()); + } + + @Test + public void toProtoSpanKind() { + assertThat(SpanAdapter.toProtoSpanKind(Kind.INTERNAL)).isEqualTo(SpanKind.INTERNAL); + assertThat(SpanAdapter.toProtoSpanKind(Kind.CLIENT)).isEqualTo(SpanKind.CLIENT); + assertThat(SpanAdapter.toProtoSpanKind(Kind.SERVER)).isEqualTo(SpanKind.SERVER); + assertThat(SpanAdapter.toProtoSpanKind(Kind.PRODUCER)).isEqualTo(SpanKind.PRODUCER); + assertThat(SpanAdapter.toProtoSpanKind(Kind.CONSUMER)).isEqualTo(SpanKind.CONSUMER); + } + + @Test + public void toProtoStatus() { + assertThat(SpanAdapter.toStatusProto(io.opentelemetry.trace.Status.OK)) + .isEqualTo(Status.newBuilder().setCode(StatusCode.Ok).build()); + assertThat( + SpanAdapter.toStatusProto( + io.opentelemetry.trace.Status.CANCELLED.withDescription("CANCELLED"))) + .isEqualTo( + Status.newBuilder().setCode(StatusCode.Cancelled).setMessage("CANCELLED").build()); + assertThat( + SpanAdapter.toStatusProto( + io.opentelemetry.trace.Status.UNKNOWN.withDescription("UNKNOWN"))) + .isEqualTo( + Status.newBuilder().setCode(StatusCode.UnknownError).setMessage("UNKNOWN").build()); + assertThat( + SpanAdapter.toStatusProto( + io.opentelemetry.trace.Status.INVALID_ARGUMENT.withDescription("INVALID_ARGUMENT"))) + .isEqualTo( + Status.newBuilder() + .setCode(StatusCode.InvalidArgument) + .setMessage("INVALID_ARGUMENT") + .build()); + assertThat( + SpanAdapter.toStatusProto( + io.opentelemetry.trace.Status.DEADLINE_EXCEEDED.withDescription( + "DEADLINE_EXCEEDED"))) + .isEqualTo( + Status.newBuilder() + .setCode(StatusCode.DeadlineExceeded) + .setMessage("DEADLINE_EXCEEDED") + .build()); + assertThat( + SpanAdapter.toStatusProto( + io.opentelemetry.trace.Status.NOT_FOUND.withDescription("NOT_FOUND"))) + .isEqualTo( + Status.newBuilder().setCode(StatusCode.NotFound).setMessage("NOT_FOUND").build()); + assertThat( + SpanAdapter.toStatusProto( + io.opentelemetry.trace.Status.ALREADY_EXISTS.withDescription("ALREADY_EXISTS"))) + .isEqualTo( + Status.newBuilder() + .setCode(StatusCode.AlreadyExists) + .setMessage("ALREADY_EXISTS") + .build()); + assertThat( + SpanAdapter.toStatusProto( + io.opentelemetry.trace.Status.PERMISSION_DENIED.withDescription( + "PERMISSION_DENIED"))) + .isEqualTo( + Status.newBuilder() + .setCode(StatusCode.PermissionDenied) + .setMessage("PERMISSION_DENIED") + .build()); + assertThat( + SpanAdapter.toStatusProto( + io.opentelemetry.trace.Status.RESOURCE_EXHAUSTED.withDescription( + "RESOURCE_EXHAUSTED"))) + .isEqualTo( + Status.newBuilder() + .setCode(StatusCode.ResourceExhausted) + .setMessage("RESOURCE_EXHAUSTED") + .build()); + assertThat( + SpanAdapter.toStatusProto( + io.opentelemetry.trace.Status.FAILED_PRECONDITION.withDescription( + "FAILED_PRECONDITION"))) + .isEqualTo( + Status.newBuilder() + .setCode(StatusCode.FailedPrecondition) + .setMessage("FAILED_PRECONDITION") + .build()); + assertThat( + SpanAdapter.toStatusProto( + io.opentelemetry.trace.Status.ABORTED.withDescription("ABORTED"))) + .isEqualTo(Status.newBuilder().setCode(StatusCode.Aborted).setMessage("ABORTED").build()); + assertThat( + SpanAdapter.toStatusProto( + io.opentelemetry.trace.Status.OUT_OF_RANGE.withDescription("OUT_OF_RANGE"))) + .isEqualTo( + Status.newBuilder().setCode(StatusCode.OutOfRange).setMessage("OUT_OF_RANGE").build()); + assertThat( + SpanAdapter.toStatusProto( + io.opentelemetry.trace.Status.UNIMPLEMENTED.withDescription("UNIMPLEMENTED"))) + .isEqualTo( + Status.newBuilder() + .setCode(StatusCode.Unimplemented) + .setMessage("UNIMPLEMENTED") + .build()); + assertThat( + SpanAdapter.toStatusProto( + io.opentelemetry.trace.Status.INTERNAL.withDescription("INTERNAL"))) + .isEqualTo( + Status.newBuilder().setCode(StatusCode.InternalError).setMessage("INTERNAL").build()); + assertThat( + SpanAdapter.toStatusProto( + io.opentelemetry.trace.Status.UNAVAILABLE.withDescription("UNAVAILABLE"))) + .isEqualTo( + Status.newBuilder().setCode(StatusCode.Unavailable).setMessage("UNAVAILABLE").build()); + assertThat( + SpanAdapter.toStatusProto( + io.opentelemetry.trace.Status.DATA_LOSS.withDescription("DATA_LOSS"))) + .isEqualTo( + Status.newBuilder().setCode(StatusCode.DataLoss).setMessage("DATA_LOSS").build()); + assertThat( + SpanAdapter.toStatusProto( + io.opentelemetry.trace.Status.UNAUTHENTICATED.withDescription("UNAUTHENTICATED"))) + .isEqualTo( + Status.newBuilder() + .setCode(StatusCode.Unauthenticated) + .setMessage("UNAUTHENTICATED") + .build()); + } + + @Test + public void toProtoSpanEvent_WithoutAttributes() { + assertThat( + SpanAdapter.toProtoSpanEvent( + TimedEvent.create( + 12345, + "test_without_attributes", + Collections.emptyMap()))) + .isEqualTo( + Span.Event.newBuilder() + .setTimeUnixnano(12345) + .setName("test_without_attributes") + .build()); + } + + @Test + public void toProtoSpanEvent_WithAttributes() { + assertThat( + SpanAdapter.toProtoSpanEvent( + TimedEvent.create( + 12345, + "test_with_attributes", + Collections.singletonMap( + "key_string", AttributeValue.stringAttributeValue("string"))))) + .isEqualTo( + Span.Event.newBuilder() + .setTimeUnixnano(12345) + .setName("test_with_attributes") + .addAttributes( + AttributeKeyValue.newBuilder() + .setKey("key_string") + .setStringValue("string") + .setType(ValueType.STRING) + .build()) + .build()); + } + + @Test + public void toProtoSpanLink_WithoutAttributes() { + assertThat(SpanAdapter.toProtoSpanLink(Link.create(SPAN_CONTEXT))) + .isEqualTo( + Span.Link.newBuilder() + .setTraceId(ByteString.copyFrom(TRACE_ID_BYTES)) + .setSpanId(ByteString.copyFrom(SPAN_ID_BYTES)) + .build()); + } + + @Test + public void toProtoSpanLink_WithAttributes() { + assertThat( + SpanAdapter.toProtoSpanLink( + Link.create( + SPAN_CONTEXT, + Collections.singletonMap( + "key_string", AttributeValue.stringAttributeValue("string"))))) + .isEqualTo( + Span.Link.newBuilder() + .setTraceId(ByteString.copyFrom(TRACE_ID_BYTES)) + .setSpanId(ByteString.copyFrom(SPAN_ID_BYTES)) + .addAttributes( + AttributeKeyValue.newBuilder() + .setKey("key_string") + .setStringValue("string") + .setType(ValueType.STRING) + .build()) + .build()); + } +} diff --git a/proto/build.gradle b/proto/build.gradle index ae6326e9d7..3f1f1f0997 100644 --- a/proto/build.gradle +++ b/proto/build.gradle @@ -10,7 +10,10 @@ description = 'OpenTelemetry Proto' ext.moduleName = 'io.opentelemetry.proto' dependencies { - api libraries.protobuf + api libraries.protobuf, + libraries.grpc_api, + libraries.grpc_protobuf, + libraries.grpc_stub signature "org.codehaus.mojo.signature:java17:1.0@signature" signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature" @@ -28,6 +31,12 @@ protobuf { // The artifact spec for the Protobuf Compiler artifact = "com.google.protobuf:protoc:${protocVersion}" } + plugins { + grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } + } + generateProtoTasks { + all()*.plugins { grpc {} } + } } // IntelliJ complains that the generated classes are not found, ask IntelliJ to include the