diff --git a/build.gradle b/build.gradle index 91f75cc22d1..1ff01c55b79 100644 --- a/build.gradle +++ b/build.gradle @@ -72,7 +72,7 @@ subprojects { it.options.encoding = "UTF-8" // Ignore warnings for protobuf generated files. - it.options.compilerArgs += ["-XepExcludedPaths:.*/sdk/build/generated/source/proto/.*"] + it.options.compilerArgs += ["-XepExcludedPaths:.*/build/generated/source/proto/.*"] // Enforce errorprone warnings to be errors. if (!JavaVersion.current().isJava9() && !JavaVersion.current().isJava10()) { @@ -115,6 +115,7 @@ subprojects { auto_value_annotation: "com.google.auto.value:auto-value-annotations:${autoValueVersion}", disruptor: 'com.lmax:disruptor:3.4.2', errorprone_annotation: "com.google.errorprone:error_prone_annotations:${errorProneVersion}", + grpc_api: "io.grpc:grpc-api:${grpcVersion}", grpc_context: "io.grpc:grpc-context:${grpcVersion}", guava: "com.google.guava:guava:${guavaVersion}", jsr305: "com.google.code.findbugs:jsr305:${findBugsJsr305Version}", diff --git a/exporters/jaeger/README.md b/exporters/jaeger/README.md new file mode 100644 index 00000000000..803135d152a --- /dev/null +++ b/exporters/jaeger/README.md @@ -0,0 +1,10 @@ +# OpenTelemetry - Jaeger Exporter - gRPC + +This is the OpenTelemetry exporter, sending span data to Jaeger via gRPC. + +## Proto files + +The proto files in this repository were copied over from the [Jaeger main repository][proto-origin]. At this moment, they have to be manually synchronize, but a [discussion exists][proto-discussion] on how to properly consume them in a more appropriate manner. + +[proto-origin]: https://github.com/jaegertracing/jaeger/tree/5b8c1f40f932897b9322bf3f110d830536ae4c71/model/proto +[proto-discussion]: https://github.com/open-telemetry/opentelemetry-java/issues/235 \ No newline at end of file diff --git a/exporters/jaeger/build.gradle b/exporters/jaeger/build.gradle new file mode 100644 index 00000000000..83748f157d1 --- /dev/null +++ b/exporters/jaeger/build.gradle @@ -0,0 +1,62 @@ +description = 'OpenTelemetry - Jaeger Exporter' + +apply plugin: 'com.google.protobuf' + +def protobufVersion = '3.7.1' +def protocVersion = '3.7.1' + +buildscript { + repositories { + maven { url "https://plugins.gradle.org/m2/" } + } + dependencies { + classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.8" + } +} + +dependencies { + api project(':opentelemetry-sdk') + + implementation "io.grpc:grpc-protobuf:${grpcVersion}" + implementation "io.grpc:grpc-stub:${grpcVersion}" + + implementation "com.google.protobuf:protobuf-java:${protobufVersion}", + "com.google.protobuf:protobuf-java-util:${protobufVersion}" + + annotationProcessor libraries.auto_value + + testImplementation "io.grpc:grpc-testing:${grpcVersion}" + testRuntime "io.grpc:grpc-netty-shaded:${grpcVersion}" + + signature "org.codehaus.mojo.signature:java17:1.0@signature" + signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature" +} + +animalsniffer { + // Don't check sourceSets.jmh and sourceSets.test + sourceSets = [ + sourceSets.main + ] +} + +protobuf { + protoc { + // 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 +// generated Java directories as source folders. +idea { + module { + sourceDirs += file("build/generated/source/proto/main/java"); + // If you have additional sourceSets and/or codegen plugins, add all of them + } +} \ No newline at end of file diff --git a/exporters/jaeger/src/main/java/io/opentelemetry/exporters/jaeger/Adapter.java b/exporters/jaeger/src/main/java/io/opentelemetry/exporters/jaeger/Adapter.java new file mode 100644 index 00000000000..16d04cb3105 --- /dev/null +++ b/exporters/jaeger/src/main/java/io/opentelemetry/exporters/jaeger/Adapter.java @@ -0,0 +1,223 @@ +/* + * 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.jaeger; + +import com.google.common.annotations.VisibleForTesting; +import com.google.protobuf.util.Timestamps; +import io.opentelemetry.exporters.jaeger.proto.api_v2.Model; +import io.opentelemetry.proto.trace.v1.AttributeValue; +import io.opentelemetry.proto.trace.v1.Span; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import javax.annotation.concurrent.ThreadSafe; + +/** Adapts OpenTelemetry objects to Jaeger objects. */ +@ThreadSafe +final class Adapter { + private static final String KEY_LOG_MESSAGE = "message"; + private static final String KEY_SPAN_KIND = "span.kind"; + private static final String KEY_SPAN_STATUS_MESSAGE = "span.status.message"; + private static final String KEY_SPAN_STATUS_CODE = "span.status.code"; + + private Adapter() {} + + /** + * Converts a list of {@link Span} into a collection of Jaeger's {@link Model.Span}. + * + * @param spans the list of spans to be converted + * @return the collection of Jaeger spans + * @see #toJaeger(Span) + */ + static Collection toJaeger(List spans) { + List convertedList = new ArrayList<>(spans.size()); + for (Span span : spans) { + convertedList.add(toJaeger(span)); + } + return convertedList; + } + + /** + * Converts a single {@link Span} into a Jaeger's {@link Model.Span}. + * + * @param span the span to be converted + * @return the Jaeger span + */ + static Model.Span toJaeger(Span span) { + Model.Span.Builder target = Model.Span.newBuilder(); + + target.setTraceId(span.getTraceId()); + target.setSpanId(span.getSpanId()); + target.setOperationName(span.getName()); + target.setStartTime(span.getStartTime()); + target.setDuration(Timestamps.between(span.getStartTime(), span.getEndTime())); + + target.addAllTags(toKeyValues(span.getAttributes())); + target.addAllLogs(toJaegerLogs(span.getTimeEvents())); + target.addAllReferences(toSpanRefs(span.getLinks())); + + // add the parent span + target.addReferences( + Model.SpanRef.newBuilder() + .setTraceId(span.getTraceId()) + .setSpanId(span.getParentSpanId()) + .setRefType(Model.SpanRefType.CHILD_OF)); + + if (span.getKind() != Span.SpanKind.SPAN_KIND_UNSPECIFIED) { + target.addTags( + Model.KeyValue.newBuilder() + .setKey(KEY_SPAN_KIND) + .setVStr(span.getKind().getValueDescriptor().getName()) + .build()); + } + + target.addTags( + Model.KeyValue.newBuilder() + .setKey(KEY_SPAN_STATUS_MESSAGE) + .setVStr(span.getStatus().getMessage()) + .build()); + + target.addTags( + Model.KeyValue.newBuilder() + .setKey(KEY_SPAN_STATUS_CODE) + .setVInt64(span.getStatus().getCode()) + .build()); + + return target.build(); + } + + /** + * Converts {@link Span.TimedEvents} into a collection of Jaeger's {@link Model.Log}. + * + * @param timeEvents the timed events to be converted + * @return a collection of Jaeger logs + * @see #toJaegerLog(Span.TimedEvent) + */ + @VisibleForTesting + static Collection toJaegerLogs(Span.TimedEvents timeEvents) { + List logs = new ArrayList<>(timeEvents.getTimedEventCount()); + for (Span.TimedEvent e : timeEvents.getTimedEventList()) { + logs.add(toJaegerLog(e)); + } + return logs; + } + + /** + * Converts a {@link Span.TimedEvent} into Jaeger's {@link Model.Log}. + * + * @param timeEvent the timed event to be converted + * @return a Jaeger log + */ + @VisibleForTesting + static Model.Log toJaegerLog(Span.TimedEvent timeEvent) { + Model.Log.Builder builder = Model.Log.newBuilder(); + builder.setTimestamp(timeEvent.getTime()); + + Span.TimedEvent.Event event = timeEvent.getEvent(); + + // name is a top-level property in OpenTelemetry + builder.addFields( + Model.KeyValue.newBuilder().setKey(KEY_LOG_MESSAGE).setVStr(event.getName()).build()); + builder.addAllFields(toKeyValues(event.getAttributes())); + + return builder.build(); + } + + /** + * Converts {@link Span.Attributes} into a collection of Jaeger's {@link Model.KeyValue}. + * + * @param attributes the span attributes + * @return a collection of Jaeger key values + * @see #toKeyValue(String, AttributeValue) + */ + @VisibleForTesting + static Collection toKeyValues(Span.Attributes attributes) { + ArrayList tags = new ArrayList<>(attributes.getAttributeMapCount()); + for (Map.Entry entry : attributes.getAttributeMapMap().entrySet()) { + tags.add(toKeyValue(entry.getKey(), entry.getValue())); + } + return tags; + } + + /** + * Converts the given key and {@link AttributeValue} into Jaeger's {@link Model.KeyValue}. + * + * @param key the entry key as string + * @param value the entry value + * @return a Jaeger key value + */ + @VisibleForTesting + static Model.KeyValue toKeyValue(String key, AttributeValue value) { + Model.KeyValue.Builder builder = Model.KeyValue.newBuilder(); + builder.setKey(key); + + switch (value.getValueCase()) { + case STRING_VALUE: + builder.setVStr(value.getStringValue()); + break; + case INT_VALUE: + builder.setVInt64(value.getIntValue()); + break; + case BOOL_VALUE: + builder.setVBool(value.getBoolValue()); + break; + case DOUBLE_VALUE: + builder.setVFloat64(value.getDoubleValue()); + break; + case VALUE_NOT_SET: + break; + } + + return builder.build(); + } + + /** + * Converts {@link Span.Links} into a collection of Jaeger's {@link Model.SpanRef}. + * + * @param links the span's links property to be converted + * @return a collection of Jaeger span references + */ + @VisibleForTesting + static Collection toSpanRefs(Span.Links links) { + List spanRefs = new ArrayList<>(links.getLinkCount()); + for (Span.Link link : links.getLinkList()) { + spanRefs.add(toSpanRef(link)); + } + return spanRefs; + } + + /** + * Converts a single {@link Span.Link} into a Jaeger's {@link Model.SpanRef}. + * + * @param link the OpenTelemetry link to be converted + * @return the Jaeger span reference + */ + @VisibleForTesting + static Model.SpanRef toSpanRef(Span.Link link) { + Model.SpanRef.Builder builder = Model.SpanRef.newBuilder(); + builder.setTraceId(link.getTraceId()); + builder.setSpanId(link.getSpanId()); + + // we can assume that all links are *follows from* + // https://github.com/open-telemetry/opentelemetry-java/issues/475 + // https://github.com/open-telemetry/opentelemetry-java/pull/481/files#r312577862 + builder.setRefType(Model.SpanRefType.FOLLOWS_FROM); + + return builder.build(); + } +} diff --git a/exporters/jaeger/src/main/java/io/opentelemetry/exporters/jaeger/JaegerGrpcSpanExporter.java b/exporters/jaeger/src/main/java/io/opentelemetry/exporters/jaeger/JaegerGrpcSpanExporter.java new file mode 100644 index 00000000000..fa03e29203b --- /dev/null +++ b/exporters/jaeger/src/main/java/io/opentelemetry/exporters/jaeger/JaegerGrpcSpanExporter.java @@ -0,0 +1,208 @@ +/* + * 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.jaeger; + +import io.grpc.Deadline; +import io.grpc.ManagedChannel; +import io.grpc.StatusRuntimeException; +import io.opentelemetry.exporters.jaeger.proto.api_v2.Collector; +import io.opentelemetry.exporters.jaeger.proto.api_v2.CollectorServiceGrpc; +import io.opentelemetry.exporters.jaeger.proto.api_v2.Model; +import io.opentelemetry.proto.trace.v1.Span; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.net.InetAddress; +import java.net.UnknownHostException; +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 to Jaeger via gRPC, using Jaeger's protobuf model. */ +@ThreadSafe +public final class JaegerGrpcSpanExporter implements SpanExporter { + private static final Logger logger = Logger.getLogger(JaegerGrpcSpanExporter.class.getName()); + private static final String CLIENT_VERSION_KEY = "jaeger.version"; + private static final String CLIENT_VERSION_VALUE = "opentelemetry-java"; + private static final String HOSTNAME_KEY = "hostname"; + private static final String HOSTNAME_DEFAULT = "(unknown)"; + private static final String IP_KEY = "ip"; + private static final String IP_DEFAULT = "0.0.0.0"; + + private final CollectorServiceGrpc.CollectorServiceBlockingStub blockingStub; + private final Model.Process process; + private final ManagedChannel managedChannel; + private final long deadline; + + /** + * Creates a new Jaeger gRPC Span Reporter with the given name, using the given channel. + * + * @param serviceName this service's name. + * @param channel the channel to use when communicating with the Jaeger Collector. + * @param deadline 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 JaegerGrpcSpanExporter(String serviceName, ManagedChannel channel, long deadline) { + String hostname; + String ipv4; + + if (serviceName == null || serviceName.trim().length() == 0) { + throw new IllegalArgumentException("Service name must not be null or empty"); + } + + try { + hostname = InetAddress.getLocalHost().getHostName(); + ipv4 = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + hostname = HOSTNAME_DEFAULT; + ipv4 = IP_DEFAULT; + } + + Model.KeyValue clientTag = + Model.KeyValue.newBuilder() + .setKey(CLIENT_VERSION_KEY) + .setVStr(CLIENT_VERSION_VALUE) + .build(); + + Model.KeyValue ipv4Tag = Model.KeyValue.newBuilder().setKey(IP_KEY).setVStr(ipv4).build(); + + Model.KeyValue hostnameTag = + Model.KeyValue.newBuilder().setKey(HOSTNAME_KEY).setVStr(hostname).build(); + + this.process = + Model.Process.newBuilder() + .setServiceName(serviceName) + .addTags(clientTag) + .addTags(ipv4Tag) + .addTags(hostnameTag) + .build(); + + this.managedChannel = channel; + this.blockingStub = CollectorServiceGrpc.newBlockingStub(channel); + this.deadline = deadline; + } + + /** + * 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) { + Model.Batch.Builder builder = Model.Batch.newBuilder(); + builder.addAllSpans(Adapter.toJaeger(spans)); + builder.setProcess(this.process); + + Collector.PostSpansRequest.Builder requestBuilder = Collector.PostSpansRequest.newBuilder(); + requestBuilder.setBatch(builder.build()); + Collector.PostSpansRequest request = requestBuilder.build(); + + try { + CollectorServiceGrpc.CollectorServiceBlockingStub stub = this.blockingStub; + if (deadline > 0) { + stub = stub.withDeadline(Deadline.after(deadline, TimeUnit.MILLISECONDS)); + } + + // for now, there's nothing to check in the response object + //noinspection ResultOfMethodCallIgnored + stub.postSpans(request); + return ResultCode.SUCCESS; + } catch (StatusRuntimeException e) { + switch (e.getStatus().getCode()) { + case DEADLINE_EXCEEDED: + case UNAVAILABLE: + return ResultCode.FAILED_RETRYABLE; + default: + return ResultCode.FAILED_NONE_RETRYABLE; + } + } catch (Throwable t) { + return ResultCode.FAILED_NONE_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 String serviceName; + private ManagedChannel channel; + private long deadline = 1_000; // ms + + /** + * Sets the service name to be used by this exporter. Required. + * + * @param serviceName the service name + * @return this builder's instance + */ + public Builder setServiceName(String serviceName) { + this.serviceName = serviceName; + return this; + } + + /** + * 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 deadline the max waiting time + * @return this builder's instance + */ + public Builder setDeadline(long deadline) { + this.deadline = deadline; + return this; + } + + /** + * Constructs a new instance of the exporter based on the builder's values. + * + * @return a new exporter's instance + */ + public JaegerGrpcSpanExporter build() { + return new JaegerGrpcSpanExporter(serviceName, channel, deadline); + } + } +} diff --git a/exporters/jaeger/src/main/proto/jaeger/api_v2/collector.proto b/exporters/jaeger/src/main/proto/jaeger/api_v2/collector.proto new file mode 100644 index 00000000000..712aae42708 --- /dev/null +++ b/exporters/jaeger/src/main/proto/jaeger/api_v2/collector.proto @@ -0,0 +1,34 @@ +/* + * 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. + */ + +syntax="proto3"; + +package jaeger.api_v2; + +import "jaeger/api_v2/model.proto"; + +option java_package = "io.opentelemetry.exporters.jaeger.proto.api_v2"; + +message PostSpansRequest { + Batch batch = 1; +} + +message PostSpansResponse { +} + +service CollectorService { + rpc PostSpans(PostSpansRequest) returns (PostSpansResponse) {} +} diff --git a/exporters/jaeger/src/main/proto/jaeger/api_v2/model.proto b/exporters/jaeger/src/main/proto/jaeger/api_v2/model.proto new file mode 100644 index 00000000000..4c944640a4e --- /dev/null +++ b/exporters/jaeger/src/main/proto/jaeger/api_v2/model.proto @@ -0,0 +1,100 @@ +/* + * 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. + */ + +syntax="proto3"; + +package jaeger.api_v2; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; + +option java_package = "io.opentelemetry.exporters.jaeger.proto.api_v2"; + +enum ValueType { + STRING = 0; + BOOL = 1; + INT64 = 2; + FLOAT64 = 3; + BINARY = 4; +}; + +message Log { + google.protobuf.Timestamp timestamp = 1; + repeated KeyValue fields = 2; +} + +message KeyValue { + string key = 1; + ValueType v_type = 2; + string v_str = 3; + bool v_bool = 4; + int64 v_int64 = 5; + double v_float64 = 6; + bytes v_binary = 7; +} + +enum SpanRefType { + CHILD_OF = 0; + FOLLOWS_FROM = 1; +}; + +message SpanRef { + bytes trace_id = 1; + bytes span_id = 2; + SpanRefType ref_type = 3; +} + +message Process { + string service_name = 1; + repeated KeyValue tags = 2; +} + +message Span { + bytes trace_id = 1; + bytes span_id = 2; + string operation_name = 3; + repeated SpanRef references = 4; + uint32 flags = 5; + google.protobuf.Timestamp start_time = 6; + google.protobuf.Duration duration = 7; + repeated KeyValue tags = 8; + repeated Log logs = 9; + Process process = 10; + string process_id = 11; + repeated string warnings = 12; +} + +message Trace { + message ProcessMapping { + string process_id = 1; + Process process = 2; + } + repeated Span spans = 1; + repeated ProcessMapping process_map = 2; + repeated string warnings = 3; +} + +message Batch { + repeated Span spans = 1; + Process process = 2; +} + +message DependencyLink { + string parent = 1; + string child = 2; + uint64 call_count = 3; + string source = 4; +} diff --git a/exporters/jaeger/src/test/java/io/opentelemetry/exporters/jaeger/AdapterTest.java b/exporters/jaeger/src/test/java/io/opentelemetry/exporters/jaeger/AdapterTest.java new file mode 100644 index 00000000000..56ead177309 --- /dev/null +++ b/exporters/jaeger/src/test/java/io/opentelemetry/exporters/jaeger/AdapterTest.java @@ -0,0 +1,275 @@ +/* + * 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.jaeger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Timestamp; +import io.opentelemetry.exporters.jaeger.proto.api_v2.Model; +import io.opentelemetry.proto.trace.v1.AttributeValue; +import io.opentelemetry.proto.trace.v1.Span; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; +import org.junit.Test; + +public class AdapterTest { + + @Test + public void testProtoSpans() { + long duration = 900; // ms + long startMs = System.currentTimeMillis(); + long endMs = startMs + duration; + Timestamp startTime = toTimestamp(startMs); + Timestamp endTime = toTimestamp(endMs); + + Span span = getProtoSpan(startTime, endTime); + List spans = Collections.singletonList(span); + + Collection jaegerSpans = Adapter.toJaeger(spans); + + // the span contents are checked somewhere else + assertEquals(1, jaegerSpans.size()); + } + + @Test + public void testProtoSpan() { + long duration = 900; // ms + long startMs = System.currentTimeMillis(); + long endMs = startMs + duration; + Timestamp startTime = toTimestamp(startMs); + Timestamp endTime = toTimestamp(endMs); + + Span span = getProtoSpan(startTime, endTime); + + // test + Model.Span jaegerSpan = Adapter.toJaeger(span); + assertEquals("abc123", jaegerSpan.getTraceId().toStringUtf8()); + assertEquals("def456", jaegerSpan.getSpanId().toStringUtf8()); + assertEquals("GET /api/endpoint", jaegerSpan.getOperationName()); + assertEquals(startTime, jaegerSpan.getStartTime()); + assertEquals(duration, jaegerSpan.getDuration().getNanos() / 1000000); + + assertEquals(4, jaegerSpan.getTagsCount()); + assertEquals("SERVER", getValue(jaegerSpan.getTagsList(), "span.kind").getVStr()); + assertEquals(0, getValue(jaegerSpan.getTagsList(), "span.status.code").getVInt64()); + assertEquals("", getValue(jaegerSpan.getTagsList(), "span.status.message").getVStr()); + + assertEquals(1, jaegerSpan.getLogsCount()); + Model.Log log = jaegerSpan.getLogs(0); + assertEquals("the log message", getValue(log.getFieldsList(), "message").getVStr()); + assertEquals("bar", getValue(log.getFieldsList(), "foo").getVStr()); + + assertEquals(2, jaegerSpan.getReferencesCount()); + + assertHasFollowsFrom(jaegerSpan); + assertHasParent(jaegerSpan); + } + + @Test + public void testJaegerLogs() { + // prepare + Span.TimedEvents timedEvents = getTimedEvents(); + + // test + Collection logs = Adapter.toJaegerLogs(timedEvents); + + // verify + assertEquals(1, logs.size()); + } + + @Test + public void testJaegerLog() { + // prepare + Span.TimedEvent timedEvent = getTimedEvent(); + + // test + Model.Log log = Adapter.toJaegerLog(timedEvent); + + // verify + assertEquals(2, log.getFieldsCount()); + + assertEquals("the log message", getValue(log.getFieldsList(), "message").getVStr()); + assertEquals("bar", getValue(log.getFieldsList(), "foo").getVStr()); + } + + @Test + public void testKeyValues() { + // prepare + AttributeValue valueB = AttributeValue.newBuilder().setBoolValue(true).build(); + Span.Attributes attributes = + Span.Attributes.newBuilder().putAttributeMap("valueB", valueB).build(); + + // test + Collection keyValues = Adapter.toKeyValues(attributes); + + // verify + // the actual content is checked in some other test + assertEquals(1, keyValues.size()); + } + + @Test + public void testKeyValue() { + // prepare + AttributeValue valueB = AttributeValue.newBuilder().setBoolValue(true).build(); + AttributeValue valueD = AttributeValue.newBuilder().setDoubleValue(1.).build(); + AttributeValue valueI = AttributeValue.newBuilder().setIntValue(2).build(); + AttributeValue valueS = AttributeValue.newBuilder().setStringValue("foobar").build(); + + // test + Model.KeyValue kvB = Adapter.toKeyValue("valueB", valueB); + Model.KeyValue kvD = Adapter.toKeyValue("valueD", valueD); + Model.KeyValue kvI = Adapter.toKeyValue("valueI", valueI); + Model.KeyValue kvS = Adapter.toKeyValue("valueS", valueS); + + // verify + assertTrue(kvB.getVBool()); + assertEquals(1., kvD.getVFloat64(), 0); + assertEquals(2, kvI.getVInt64()); + assertEquals("foobar", kvS.getVStr()); + assertEquals("foobar", kvS.getVStrBytes().toStringUtf8()); + } + + @Test + public void testSpanRefs() { + // prepare + Span.Link link = + Span.Link.newBuilder() + .setSpanId(ByteString.copyFromUtf8("abc123")) + .setTraceId(ByteString.copyFromUtf8("def456")) + .build(); + + Span.Links links = Span.Links.newBuilder().addLink(link).build(); + + // test + Collection spanRefs = Adapter.toSpanRefs(links); + + // verify + assertEquals(1, spanRefs.size()); // the actual span ref is tested in another test + } + + @Test + public void testSpanRef() { + // prepare + Span.Link link = + Span.Link.newBuilder() + .setSpanId(ByteString.copyFromUtf8("abc123")) + .setTraceId(ByteString.copyFromUtf8("def456")) + .build(); + + // test + Model.SpanRef spanRef = Adapter.toSpanRef(link); + + // verify + assertEquals("abc123", spanRef.getSpanId().toStringUtf8()); + assertEquals("def456", spanRef.getTraceId().toStringUtf8()); + assertEquals(Model.SpanRefType.FOLLOWS_FROM, spanRef.getRefType()); + } + + public Span.TimedEvents getTimedEvents() { + Span.TimedEvent timedEvent = getTimedEvent(); + return Span.TimedEvents.newBuilder().addTimedEvent(timedEvent).build(); + } + + private Span.TimedEvent getTimedEvent() { + long ms = System.currentTimeMillis(); + Timestamp ts = toTimestamp(ms); + AttributeValue valueS = AttributeValue.newBuilder().setStringValue("bar").build(); + Span.Attributes attributes = + Span.Attributes.newBuilder().putAttributeMap("foo", valueS).build(); + + return Span.TimedEvent.newBuilder() + .setTime(ts) + .setEvent( + Span.TimedEvent.Event.newBuilder() + .setName("the log message") + .setAttributes(attributes) + .build()) + .build(); + } + + private Span getProtoSpan(Timestamp startTime, Timestamp endTime) { + AttributeValue valueB = AttributeValue.newBuilder().setBoolValue(true).build(); + Span.Attributes attributes = + Span.Attributes.newBuilder().putAttributeMap("valueB", valueB).build(); + + Span.Link link = + Span.Link.newBuilder() + .setTraceId(ByteString.copyFromUtf8("parent123")) + .setSpanId(ByteString.copyFromUtf8("parent456")) + .build(); + + Span.Links links = Span.Links.newBuilder().addLink(link).build(); + + return Span.newBuilder() + .setTraceId(ByteString.copyFromUtf8("abc123")) + .setSpanId(ByteString.copyFromUtf8("def456")) + .setParentSpanId(ByteString.copyFromUtf8("parent789")) + .setName("GET /api/endpoint") + .setStartTime(startTime) + .setEndTime(endTime) + .setAttributes(attributes) + .setTimeEvents(getTimedEvents()) + .setLinks(links) + .setKind(Span.SpanKind.SERVER) + .build(); + } + + Timestamp toTimestamp(long ms) { + return Timestamp.newBuilder() + .setSeconds(ms / 1000) + .setNanos((int) ((ms % 1000) * 1000000)) + .build(); + } + + @Nullable + private static Model.KeyValue getValue(List tagsList, String s) { + for (Model.KeyValue kv : tagsList) { + if (kv.getKey().equals(s)) { + return kv; + } + } + return null; + } + + private static void assertHasFollowsFrom(Model.Span jaegerSpan) { + boolean found = false; + for (Model.SpanRef spanRef : jaegerSpan.getReferencesList()) { + if (Model.SpanRefType.FOLLOWS_FROM.equals(spanRef.getRefType())) { + assertEquals("parent123", spanRef.getTraceId().toStringUtf8()); + assertEquals("parent456", spanRef.getSpanId().toStringUtf8()); + found = true; + } + } + assertTrue("Should have found the follows-from reference", found); + } + + private static void assertHasParent(Model.Span jaegerSpan) { + boolean found = false; + for (Model.SpanRef spanRef : jaegerSpan.getReferencesList()) { + if (Model.SpanRefType.CHILD_OF.equals(spanRef.getRefType())) { + assertEquals("abc123", spanRef.getTraceId().toStringUtf8()); + assertEquals("parent789", spanRef.getSpanId().toStringUtf8()); + found = true; + } + } + assertTrue("Should have found the parent reference", found); + } +} diff --git a/exporters/jaeger/src/test/java/io/opentelemetry/exporters/jaeger/JaegerGrpcSpanExporterTest.java b/exporters/jaeger/src/test/java/io/opentelemetry/exporters/jaeger/JaegerGrpcSpanExporterTest.java new file mode 100644 index 00000000000..e9542240d19 --- /dev/null +++ b/exporters/jaeger/src/test/java/io/opentelemetry/exporters/jaeger/JaegerGrpcSpanExporterTest.java @@ -0,0 +1,142 @@ +/* + * 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.jaeger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Timestamp; +import io.grpc.ManagedChannel; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.GrpcCleanupRule; +import io.opentelemetry.exporters.jaeger.proto.api_v2.Collector; +import io.opentelemetry.exporters.jaeger.proto.api_v2.CollectorServiceGrpc; +import io.opentelemetry.exporters.jaeger.proto.api_v2.Model; +import io.opentelemetry.proto.trace.v1.Span; +import java.net.InetAddress; +import java.util.Collections; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; + +public class JaegerGrpcSpanExporterTest { + + @Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + + private final CollectorServiceGrpc.CollectorServiceImplBase service = + mock( + CollectorServiceGrpc.CollectorServiceImplBase.class, + delegatesTo(new MockCollectorService())); + + @Test + public void testExport() throws Exception { + String serverName = InProcessServerBuilder.generateName(); + ArgumentCaptor requestCaptor = + ArgumentCaptor.forClass(Collector.PostSpansRequest.class); + + grpcCleanup.register( + InProcessServerBuilder.forName(serverName) + .directExecutor() + .addService(service) + .build() + .start()); + + ManagedChannel channel = + grpcCleanup.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()); + + long duration = 900; // ms + long startMs = System.currentTimeMillis(); + long endMs = startMs + duration; + Timestamp startTime = + Timestamp.newBuilder() + .setSeconds(startMs / 1000) + .setNanos((int) ((startMs % 1000) * 1000000)) + .build(); + Timestamp endTime = + Timestamp.newBuilder() + .setSeconds(endMs / 1000) + .setNanos((int) ((endMs % 1000) * 1000000)) + .build(); + Span span = + Span.newBuilder() + .setTraceId(ByteString.copyFromUtf8("abc123")) + .setSpanId(ByteString.copyFromUtf8("def456")) + .setName("GET /api/endpoint") + .setStartTime(startTime) + .setEndTime(endTime) + .build(); + + // test + JaegerGrpcSpanExporter exporter = + JaegerGrpcSpanExporter.newBuilder().setServiceName("test").setChannel(channel).build(); + exporter.export(Collections.singletonList(span)); + + // verify + verify(service) + .postSpans( + requestCaptor.capture(), + ArgumentMatchers.>any()); + + Model.Batch batch = requestCaptor.getValue().getBatch(); + assertEquals(1, batch.getSpansCount()); + assertEquals("GET /api/endpoint", batch.getSpans(0).getOperationName()); + assertEquals("abc123", batch.getSpans(0).getTraceId().toStringUtf8()); + assertEquals("def456", batch.getSpans(0).getSpanId().toStringUtf8()); + assertEquals("test", batch.getProcess().getServiceName()); + assertEquals(3, batch.getProcess().getTagsCount()); + + boolean foundClientTag = false; + boolean foundHostname = false; + boolean foundIp = false; + for (Model.KeyValue kv : batch.getProcess().getTagsList()) { + if (kv.getKey().equals("jaeger.version")) { + foundClientTag = true; + assertEquals("opentelemetry-java", kv.getVStr()); + } + + if (kv.getKey().equals("ip")) { + foundIp = true; + assertEquals(InetAddress.getLocalHost().getHostAddress(), kv.getVStr()); + } + + if (kv.getKey().equals("hostname")) { + foundHostname = true; + assertEquals(InetAddress.getLocalHost().getHostName(), kv.getVStr()); + } + } + assertTrue("a client tag should have been present", foundClientTag); + assertTrue("an ip tag should have been present", foundIp); + assertTrue("a hostname tag should have been present", foundHostname); + } + + static class MockCollectorService extends CollectorServiceGrpc.CollectorServiceImplBase { + @Override + public void postSpans( + Collector.PostSpansRequest request, + StreamObserver responseObserver) { + responseObserver.onNext(Collector.PostSpansResponse.newBuilder().build()); + responseObserver.onCompleted(); + } + } +} diff --git a/settings.gradle b/settings.gradle index 015e766026b..955be856416 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,6 +4,7 @@ include ":opentelemetry-all" include ":opentelemetry-api" include ":opentelemetry-contrib-runtime-metrics" include ":opentelemetry-contrib-trace-utils" +include ":opentelemetry-exporters-jaeger" include ":opentelemetry-opentracing-shim" include ":opentelemetry-sdk" include ":opentelemetry-sdk-contrib-async-processor" @@ -13,6 +14,7 @@ project(':opentelemetry-api').projectDir = "$rootDir/api" as File project(':opentelemetry-contrib-runtime-metrics').projectDir = "$rootDir/contrib/runtime_metrics" as File project(':opentelemetry-contrib-trace-utils').projectDir = "$rootDir/contrib/trace_utils" as File +project(':opentelemetry-exporters-jaeger').projectDir = "$rootDir/exporters/jaeger" as File project(':opentelemetry-opentracing-shim').projectDir = "$rootDir/opentracing_shim" as File project(':opentelemetry-sdk').projectDir = "$rootDir/sdk" as File project(':opentelemetry-sdk-contrib-async-processor').projectDir =