aboutsummaryrefslogtreecommitdiffstats
path: root/exporters
diff options
context:
space:
mode:
authorMarc Carré <marccarre@users.noreply.github.com>2018-03-16 01:03:49 +0000
committerHailong Wen <youxiabsyw@gmail.com>2018-03-15 18:03:49 -0700
commit8190805f302dfe4282a69b1c077dc6097abebdd2 (patch)
treeb545366faad899c060b48964f7221cb3504b5890 /exporters
parente43d9c496557a388bffa61e33f39c305d6d86740 (diff)
downloadplatform_external_opencensus-java-8190805f302dfe4282a69b1c077dc6097abebdd2.tar.gz
platform_external_opencensus-java-8190805f302dfe4282a69b1c077dc6097abebdd2.tar.bz2
platform_external_opencensus-java-8190805f302dfe4282a69b1c077dc6097abebdd2.zip
Add traces exporter to Jaeger (#1023)
* Add git-ignores. * Add traces exporter to Jaeger (+tests). * Add string tag with events' description to Jaeger exporter (+tests). * Add integration test for Jaeger exporter (manually run).
Diffstat (limited to 'exporters')
-rw-r--r--exporters/trace/jaeger/README.md90
-rw-r--r--exporters/trace/jaeger/build.gradle18
-rw-r--r--exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandler.java309
-rw-r--r--exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporter.java112
-rw-r--r--exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerIntegrationTest.java227
-rw-r--r--exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerTest.java186
-rw-r--r--exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporterTest.java52
7 files changed, 994 insertions, 0 deletions
diff --git a/exporters/trace/jaeger/README.md b/exporters/trace/jaeger/README.md
new file mode 100644
index 00000000..461f3e5b
--- /dev/null
+++ b/exporters/trace/jaeger/README.md
@@ -0,0 +1,90 @@
+# OpenCensus Jaeger Trace Exporter
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Jaeger Trace Exporter* is a trace exporter that exports
+data to Jaeger.
+
+[Jaeger](https://jaeger.readthedocs.io/en/latest/), inspired by [Dapper](https://research.google.com/pubs/pub36356.html) and [OpenZipkin](http://zipkin.io/), is a distributed tracing system released as open source by [Uber Technologies](http://uber.github.io/). It is used for monitoring and troubleshooting microservices-based distributed systems, including:
+
+- Distributed context propagation
+- Distributed transaction monitoring
+- Root cause analysis
+- Service dependency analysis
+- Performance / latency optimization
+
+## Quickstart
+
+### Prerequisites
+
+[Jaeger](https://jaeger.readthedocs.io/en/latest/) stores and queries traces exported by
+applications instrumented with Census. The easiest way to [start a Jaeger
+server](https://jaeger.readthedocs.io/en/latest/getting_started/) is to paste the below:
+
+```bash
+docker run -d \
+ -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
+ -p5775:5775/udp -p6831:6831/udp -p6832:6832/udp \
+ -p5778:5778 -p16686:16686 -p14268:14268 -p9411:9411 \
+ jaegertracing/all-in-one:latest
+```
+
+### Hello Jaeger
+
+#### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-api</artifactId>
+ <version>0.13.0</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-trace-jaeger</artifactId>
+ <version>0.13.0</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.13.0</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+compile 'io.opencensus:opencensus-api:0.13.0'
+compile 'io.opencensus:opencensus-exporter-trace-jaeger:0.13.0'
+runtime 'io.opencensus:opencensus-impl:0.13.0'
+```
+
+#### Register the exporter
+
+This will export traces to the Jaeger thrift format to the Jaeger instance started previously:
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) throws Exception {
+ JaegerTraceExporter.createAndRegister("http://127.0.0.1:14268/api/traces", "my-service");
+ // ...
+ }
+}
+```
+
+See also [this integration test](https://github.com/census-instrumentation/opencensus-java/blob/master/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerIntegrationTest.java).
+
+#### Java Versions
+
+Java 6 or above is required for using this exporter.
+
+[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master
+[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java
+[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true
+[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master
+[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-jaeger/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-jaeger
diff --git a/exporters/trace/jaeger/build.gradle b/exporters/trace/jaeger/build.gradle
new file mode 100644
index 00000000..1e005bed
--- /dev/null
+++ b/exporters/trace/jaeger/build.gradle
@@ -0,0 +1,18 @@
+description = 'OpenCensus Trace Jaeger Exporter'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.6
+ it.targetCompatibility = 1.6
+}
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.jaeger_reporter
+
+ testCompile 'org.hamcrest:hamcrest-junit:2.0.0.0'
+ testCompile 'com.google.http-client:google-http-client-gson:1.23.0'
+ testCompile project(':opencensus-api')
+ testRuntime project(':opencensus-impl')
+
+ signature "org.codehaus.mojo.signature:java16:+@signature"
+}
diff --git a/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandler.java b/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandler.java
new file mode 100644
index 00000000..395b8169
--- /dev/null
+++ b/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandler.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright 2018, OpenCensus 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.opencensus.exporter.trace.jaeger;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Ints;
+import com.google.common.primitives.Longs;
+import com.google.errorprone.annotations.MustBeClosed;
+import com.uber.jaeger.senders.HttpSender;
+import com.uber.jaeger.thriftjava.Log;
+import com.uber.jaeger.thriftjava.Process;
+import com.uber.jaeger.thriftjava.Span;
+import com.uber.jaeger.thriftjava.SpanRef;
+import com.uber.jaeger.thriftjava.SpanRefType;
+import com.uber.jaeger.thriftjava.Tag;
+import com.uber.jaeger.thriftjava.TagType;
+import io.opencensus.common.Function;
+import io.opencensus.common.Scope;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+import org.apache.thrift.TException;
+
+@NotThreadSafe
+final class JaegerExporterHandler extends SpanExporter.Handler {
+ private static final String EXPORT_SPAN_NAME = "ExportJaegerTraces";
+ private static final String DESCRIPTION = "description";
+
+ private static final Logger logger = Logger.getLogger(JaegerExporterHandler.class.getName());
+
+ /**
+ * Sampler with low probability used during the export in order to avoid the case when user sets
+ * the default sampler to always sample and we get the Thrift span of the Jaeger export call
+ * always sampled and go to an infinite loop.
+ */
+ private static final Sampler lowProbabilitySampler = Samplers.probabilitySampler(0.0001);
+
+ private static final Tracer tracer = Tracing.getTracer();
+
+ private static final Function<? super String, Tag> stringAttributeConverter =
+ new Function<String, Tag>() {
+ @Override
+ public Tag apply(final String value) {
+ final Tag tag = new Tag();
+ tag.setVType(TagType.STRING);
+ tag.setVStr(value);
+ return tag;
+ }
+ };
+
+ private static final Function<? super Boolean, Tag> booleanAttributeConverter =
+ new Function<Boolean, Tag>() {
+ @Override
+ public Tag apply(final Boolean value) {
+ final Tag tag = new Tag();
+ tag.setVType(TagType.BOOL);
+ tag.setVBool(value);
+ return tag;
+ }
+ };
+
+ private static final Function<? super Long, Tag> longAttributeConverter =
+ new Function<Long, Tag>() {
+ @Override
+ public Tag apply(final Long value) {
+ final Tag tag = new Tag();
+ tag.setVType(TagType.LONG);
+ tag.setVLong(value);
+ return tag;
+ }
+ };
+
+ private static final Function<Object, Tag> defaultAttributeConverter =
+ new Function<Object, Tag>() {
+ @Override
+ public Tag apply(final Object value) {
+ final Tag tag = new Tag();
+ tag.setVType(TagType.STRING);
+ tag.setVStr(value.toString());
+ return tag;
+ }
+ };
+
+ // Re-usable buffers to avoid too much memory allocation during conversions.
+ // N.B.: these make instances of this class thread-unsafe, hence the above
+ // @NotThreadSafe annotation.
+ private final byte[] spanIdBuffer = new byte[SpanId.SIZE];
+ private final byte[] traceIdBuffer = new byte[TraceId.SIZE];
+ private final byte[] optionsBuffer = new byte[Integer.SIZE / Byte.SIZE];
+
+ private final HttpSender sender;
+ private final Process process;
+
+ JaegerExporterHandler(final HttpSender sender, final Process process) {
+ this.sender = checkNotNull(sender, "Jaeger sender must NOT be null.");
+ this.process = checkNotNull(process, "Process sending traces must NOT be null.");
+ }
+
+ @Override
+ public void export(final Collection<SpanData> spanDataList) {
+ final Scope exportScope = newExportScope();
+ try {
+ doExport(spanDataList);
+ } catch (TException e) {
+ tracer
+ .getCurrentSpan() // exportScope above.
+ .setStatus(Status.UNKNOWN.withDescription(getMessageOrDefault(e)));
+ logger.log(Level.WARNING, "Failed to export traces to Jaeger: " + e);
+ } finally {
+ exportScope.close();
+ }
+ }
+
+ @MustBeClosed
+ private static Scope newExportScope() {
+ // Start a new span with explicit sampler (with low probability) to avoid the case when user
+ // sets the default sampler to always sample and we get the Thrift span of the Jaeger
+ // export call always sampled and go to an infinite loop.
+ return tracer.spanBuilder(EXPORT_SPAN_NAME).setSampler(lowProbabilitySampler).startScopedSpan();
+ }
+
+ private void doExport(final Collection<SpanData> spanDataList) throws TException {
+ final List<Span> spans = spanDataToJaegerThriftSpans(spanDataList);
+ sender.send(process, spans);
+ }
+
+ private static String getMessageOrDefault(final TException e) {
+ return e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
+ }
+
+ private List<Span> spanDataToJaegerThriftSpans(final Collection<SpanData> spanDataList) {
+ final List<Span> spans = Lists.newArrayListWithExpectedSize(spanDataList.size());
+ for (final SpanData spanData : spanDataList) {
+ spans.add(spanDataToJaegerThriftSpan(spanData));
+ }
+ return spans;
+ }
+
+ private Span spanDataToJaegerThriftSpan(final SpanData spanData) {
+ final long startTimeInMicros = timestampToMicros(spanData.getStartTimestamp());
+ final long endTimeInMicros = timestampToMicros(spanData.getEndTimestamp());
+
+ final SpanContext context = spanData.getContext();
+ copyToBuffer(context.getTraceId());
+
+ return new com.uber.jaeger.thriftjava.Span(
+ traceIdLow(),
+ traceIdHigh(),
+ spanIdToLong(context.getSpanId()),
+ spanIdToLong(spanData.getParentSpanId()),
+ spanData.getName(),
+ optionsToFlags(context.getTraceOptions()),
+ startTimeInMicros,
+ endTimeInMicros - startTimeInMicros)
+ .setReferences(linksToReferences(spanData.getLinks().getLinks()))
+ .setTags(attributesToTags(spanData.getAttributes().getAttributeMap()))
+ .setLogs(annotationEventsToLogs(spanData.getAnnotations().getEvents()));
+ }
+
+ private void copyToBuffer(final TraceId traceId) {
+ // Attempt to minimise allocations, since TraceId#getBytes currently creates a defensive copy:
+ traceId.copyBytesTo(traceIdBuffer, 0);
+ }
+
+ private long traceIdHigh() {
+ return Longs.fromBytes(
+ traceIdBuffer[0],
+ traceIdBuffer[1],
+ traceIdBuffer[2],
+ traceIdBuffer[3],
+ traceIdBuffer[4],
+ traceIdBuffer[5],
+ traceIdBuffer[6],
+ traceIdBuffer[7]);
+ }
+
+ private long traceIdLow() {
+ return Longs.fromBytes(
+ traceIdBuffer[8],
+ traceIdBuffer[9],
+ traceIdBuffer[10],
+ traceIdBuffer[11],
+ traceIdBuffer[12],
+ traceIdBuffer[13],
+ traceIdBuffer[14],
+ traceIdBuffer[15]);
+ }
+
+ private long spanIdToLong(final @Nullable SpanId spanId) {
+ if (spanId == null) {
+ return 0L;
+ }
+ // Attempt to minimise allocations, since SpanId#getBytes currently creates a defensive copy:
+ spanId.copyBytesTo(spanIdBuffer, 0);
+ return Longs.fromByteArray(spanIdBuffer);
+ }
+
+ private int optionsToFlags(final TraceOptions traceOptions) {
+ // Attempt to minimise allocations, since TraceOptions#getBytes currently creates a defensive
+ // copy:
+ traceOptions.copyBytesTo(optionsBuffer, optionsBuffer.length - 1);
+ return Ints.fromByteArray(optionsBuffer);
+ }
+
+ private List<SpanRef> linksToReferences(final List<Link> links) {
+ final List<SpanRef> spanRefs = Lists.newArrayListWithExpectedSize(links.size());
+ for (final Link link : links) {
+ copyToBuffer(link.getTraceId());
+ spanRefs.add(
+ new SpanRef(
+ linkTypeToRefType(link.getType()),
+ traceIdLow(),
+ traceIdHigh(),
+ spanIdToLong(link.getSpanId())));
+ }
+ return spanRefs;
+ }
+
+ private static long timestampToMicros(final @Nullable Timestamp timestamp) {
+ return (timestamp == null)
+ ? 0L
+ : SECONDS.toMicros(timestamp.getSeconds()) + NANOSECONDS.toMicros(timestamp.getNanos());
+ }
+
+ private static SpanRefType linkTypeToRefType(final Link.Type type) {
+ switch (type) {
+ case CHILD_LINKED_SPAN:
+ return SpanRefType.CHILD_OF;
+ case PARENT_LINKED_SPAN:
+ return SpanRefType.FOLLOWS_FROM;
+ }
+ throw new UnsupportedOperationException(
+ format("Failed to convert link type [%s] to a Jaeger SpanRefType.", type));
+ }
+
+ private static List<Tag> attributesToTags(final Map<String, AttributeValue> attributes) {
+ final List<Tag> tags = Lists.newArrayListWithExpectedSize(attributes.size());
+ for (final Map.Entry<String, AttributeValue> entry : attributes.entrySet()) {
+ final Tag tag =
+ entry
+ .getValue()
+ .match(
+ stringAttributeConverter,
+ booleanAttributeConverter,
+ longAttributeConverter,
+ defaultAttributeConverter);
+ tag.setKey(entry.getKey());
+ tags.add(tag);
+ }
+ return tags;
+ }
+
+ private static List<Log> annotationEventsToLogs(
+ final List<SpanData.TimedEvent<Annotation>> events) {
+ final List<Log> logs = Lists.newArrayListWithExpectedSize(events.size());
+ for (final SpanData.TimedEvent<Annotation> event : events) {
+ final long timestampsInMicros = timestampToMicros(event.getTimestamp());
+ final List<Tag> tags = attributesToTags(event.getEvent().getAttributes());
+ tags.add(descriptionToTag(event.getEvent().getDescription()));
+ final Log log = new Log(timestampsInMicros, tags);
+ logs.add(log);
+ }
+ return logs;
+ }
+
+ private static Tag descriptionToTag(final String description) {
+ final Tag tag = new Tag(DESCRIPTION, TagType.STRING);
+ tag.setVStr(description);
+ return tag;
+ }
+}
diff --git a/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporter.java b/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporter.java
new file mode 100644
index 00000000..7a9b13a5
--- /dev/null
+++ b/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporter.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2018, OpenCensus 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.opencensus.exporter.trace.jaeger;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.uber.jaeger.senders.HttpSender;
+import com.uber.jaeger.thriftjava.Process;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanExporter;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Jaeger. Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * JaegerTraceExporter.createAndRegister("http://127.0.0.1:14268/api/traces", "myservicename");
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @since 0.13
+ */
+public final class JaegerTraceExporter {
+ private static final String REGISTER_NAME = JaegerTraceExporter.class.getName();
+ private static final Object monitor = new Object();
+
+ @GuardedBy("monitor")
+ @Nullable
+ private static SpanExporter.Handler handler = null;
+
+ // Make constructor private to hide it from the API and therefore avoid users calling it.
+ private JaegerTraceExporter() {}
+
+ /**
+ * Creates and registers the Jaeger Trace exporter to the OpenCensus library. Only one Jaeger
+ * exporter can be registered at any point.
+ *
+ * @param thriftEndpoint the Thrift endpoint of your Jaeger instance, e.g.:
+ * "http://127.0.0.1:14268/api/traces"
+ * @param serviceName the local service name of the process.
+ * @throws IllegalStateException if a Jaeger exporter is already registered.
+ * @since 0.13
+ */
+ public static void createAndRegister(final String thriftEndpoint, final String serviceName) {
+ synchronized (monitor) {
+ checkState(handler == null, "Jaeger exporter is already registered.");
+ final SpanExporter.Handler newHandler = newHandler(thriftEndpoint, serviceName);
+ JaegerTraceExporter.handler = newHandler;
+ register(Tracing.getExportComponent().getSpanExporter(), newHandler);
+ }
+ }
+
+ private static SpanExporter.Handler newHandler(
+ final String thriftEndpoint, final String serviceName) {
+ final HttpSender sender = new HttpSender(thriftEndpoint);
+ final Process process = new Process(serviceName);
+ return new JaegerExporterHandler(sender, process);
+ }
+
+ /**
+ * Registers the {@link JaegerTraceExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(final SpanExporter spanExporter, final SpanExporter.Handler handler) {
+ spanExporter.registerHandler(REGISTER_NAME, handler);
+ }
+
+ /**
+ * Unregisters the {@link JaegerTraceExporter} from the OpenCensus library.
+ *
+ * @throws IllegalStateException if a Jaeger exporter is not registered.
+ * @since 0.13
+ */
+ public static void unregister() {
+ synchronized (monitor) {
+ checkState(handler != null, "Jaeger exporter is not registered.");
+ unregister(Tracing.getExportComponent().getSpanExporter());
+ handler = null;
+ }
+ }
+
+ /**
+ * Unregisters the {@link JaegerTraceExporter}.
+ *
+ * @param spanExporter the instance of the {@link SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(final SpanExporter spanExporter) {
+ spanExporter.unregisterHandler(REGISTER_NAME);
+ }
+}
diff --git a/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerIntegrationTest.java b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerIntegrationTest.java
new file mode 100644
index 00000000..d3de1efc
--- /dev/null
+++ b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerIntegrationTest.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2018, OpenCensus 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.opencensus.exporter.trace.jaeger;
+
+import static java.lang.String.format;
+import static java.lang.System.currentTimeMillis;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.hamcrest.Matchers.matchesPattern;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assume.assumeThat;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.common.base.Splitter;
+import com.google.common.collect.TreeTraverser;
+import com.google.common.io.Files;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import io.opencensus.common.Scope;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.SpanBuilder;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import java.io.File;
+import java.io.IOException;
+import java.util.Random;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.junit.Test;
+
+public class JaegerExporterHandlerIntegrationTest {
+ private static final String JAEGER_HOST = "127.0.0.1";
+ private static final String SERVICE_NAME = "test";
+ private static final String SPAN_NAME = "my.org/ProcessVideo";
+ private static final String START_PROCESSING_VIDEO = "Start processing video.";
+ private static final String FINISHED_PROCESSING_VIDEO = "Finished processing video.";
+
+ private static final Logger logger =
+ Logger.getLogger(JaegerExporterHandlerIntegrationTest.class.getName());
+ private static final Tracer tracer = Tracing.getTracer();
+
+ @Test(timeout = 30000)
+ public void exportToJaeger() throws InterruptedException, IOException {
+ assumeThat("docker is installed and executable", dockerIsInstalledAndExecutable(), is(true));
+
+ final Process jaeger =
+ Runtime.getRuntime()
+ .exec(
+ "docker run --rm "
+ + "-e COLLECTOR_ZIPKIN_HTTP_PORT=9411 -p5775:5775/udp -p6831:6831/udp "
+ + "-p6832:6832/udp -p5778:5778 -p16686:16686 -p14268:14268 -p9411:9411 "
+ + "jaegertracing/all-in-one:latest");
+ final long timeWaitingForJaegerToStart = 1000L;
+ Thread.sleep(timeWaitingForJaegerToStart);
+ final long startTime = currentTimeMillis();
+
+ try {
+ SpanBuilder spanBuilder =
+ tracer.spanBuilder(SPAN_NAME).setRecordEvents(true).setSampler(Samplers.alwaysSample());
+ JaegerTraceExporter.createAndRegister(
+ format("http://%s:14268/api/traces", JAEGER_HOST), SERVICE_NAME);
+
+ final Scope scopedSpan = spanBuilder.startScopedSpan();
+ try {
+ tracer.getCurrentSpan().addAnnotation(START_PROCESSING_VIDEO);
+ // Sleep for [0,10] milliseconds to fake work.
+ Thread.sleep(new Random().nextInt(10) + 1);
+ tracer.getCurrentSpan().putAttribute("foo", AttributeValue.stringAttributeValue("bar"));
+ tracer.getCurrentSpan().addAnnotation(FINISHED_PROCESSING_VIDEO);
+ } catch (Exception e) {
+ tracer.getCurrentSpan().addAnnotation("Exception thrown when processing video.");
+ tracer.getCurrentSpan().setStatus(Status.UNKNOWN);
+ logger.severe(e.getMessage());
+ } finally {
+ scopedSpan.close();
+ }
+
+ logger.info("Wait longer than the reporting duration...");
+ // Wait for a duration longer than reporting duration (5s) to ensure spans are exported.
+ final long timeWaitingForSpansToBeExported = 5100L;
+ Thread.sleep(timeWaitingForSpansToBeExported);
+ JaegerTraceExporter.unregister();
+ final long endTime = currentTimeMillis();
+
+ // Get traces recorded by Jaeger:
+ final HttpRequest request =
+ new NetHttpTransport()
+ .createRequestFactory()
+ .buildGetRequest(
+ new GenericUrl(
+ format(
+ "http://%s:16686/api/traces?end=%d&limit=20&lookback=1m&maxDuration&minDuration&service=%s",
+ JAEGER_HOST, MILLISECONDS.toMicros(currentTimeMillis()), SERVICE_NAME)));
+ final HttpResponse response = request.execute();
+ final String body = response.parseAsString();
+ assertThat("Response was: " + body, response.getStatusCode(), is(200));
+
+ final JsonObject result = new JsonParser().parse(body).getAsJsonObject();
+ // Pretty-print for debugging purposes:
+ logger.log(Level.FINE, new GsonBuilder().setPrettyPrinting().create().toJson(result));
+
+ assertThat(result, is(not(nullValue())));
+ assertThat(result.get("total").getAsInt(), is(0));
+ assertThat(result.get("limit").getAsInt(), is(0));
+ assertThat(result.get("offset").getAsInt(), is(0));
+ assertThat(result.get("errors").getAsJsonNull(), is(JsonNull.INSTANCE));
+ final JsonArray data = result.get("data").getAsJsonArray();
+ assertThat(data, is(not(nullValue())));
+ assertThat(data.size(), is(1));
+ final JsonObject trace = data.get(0).getAsJsonObject();
+ assertThat(trace, is(not(nullValue())));
+ assertThat(trace.get("traceID").getAsString(), matchesPattern("[a-z0-9]{31,32}"));
+
+ final JsonArray spans = trace.get("spans").getAsJsonArray();
+ assertThat(spans, is(not(nullValue())));
+ assertThat(spans.size(), is(1));
+
+ final JsonObject span = spans.get(0).getAsJsonObject();
+ assertThat(span, is(not(nullValue())));
+ assertThat(span.get("traceID").getAsString(), matchesPattern("[a-z0-9]{31,32}"));
+ assertThat(span.get("spanID").getAsString(), matchesPattern("[a-z0-9]{16}"));
+ assertThat(span.get("flags").getAsInt(), is(1));
+ assertThat(span.get("operationName").getAsString(), is(SPAN_NAME));
+ assertThat(span.get("references").getAsJsonArray().size(), is(0));
+ assertThat(
+ span.get("startTime").getAsLong(),
+ is(greaterThanOrEqualTo(MILLISECONDS.toMicros(startTime))));
+ assertThat(
+ span.get("startTime").getAsLong(), is(lessThanOrEqualTo(MILLISECONDS.toMicros(endTime))));
+ assertThat(
+ span.get("duration").getAsLong(),
+ is(greaterThanOrEqualTo(timeWaitingForJaegerToStart + timeWaitingForSpansToBeExported)));
+
+ final JsonArray tags = span.get("tags").getAsJsonArray();
+ assertThat(tags.size(), is(1));
+ final JsonObject tag = tags.get(0).getAsJsonObject();
+ assertThat(tag.get("key").getAsString(), is("foo"));
+ assertThat(tag.get("type").getAsString(), is("string"));
+ assertThat(tag.get("value").getAsString(), is("bar"));
+
+ final JsonArray logs = span.get("logs").getAsJsonArray();
+ assertThat(logs.size(), is(2));
+
+ final JsonObject log1 = logs.get(0).getAsJsonObject();
+ final long ts1 = log1.get("timestamp").getAsLong();
+ assertThat(ts1, is(greaterThanOrEqualTo(MILLISECONDS.toMicros(startTime))));
+ assertThat(ts1, is(lessThanOrEqualTo(MILLISECONDS.toMicros(endTime))));
+ final JsonArray fields1 = log1.get("fields").getAsJsonArray();
+ assertThat(fields1.size(), is(1));
+ final JsonObject field1 = fields1.get(0).getAsJsonObject();
+ assertThat(field1.get("key").getAsString(), is("description"));
+ assertThat(field1.get("type").getAsString(), is("string"));
+ assertThat(field1.get("value").getAsString(), is(START_PROCESSING_VIDEO));
+
+ final JsonObject log2 = logs.get(1).getAsJsonObject();
+ final long ts2 = log2.get("timestamp").getAsLong();
+ assertThat(ts2, is(greaterThanOrEqualTo(MILLISECONDS.toMicros(startTime))));
+ assertThat(ts2, is(lessThanOrEqualTo(MILLISECONDS.toMicros(endTime))));
+ assertThat(ts2, is(greaterThanOrEqualTo(ts1)));
+ final JsonArray fields2 = log2.get("fields").getAsJsonArray();
+ assertThat(fields2.size(), is(1));
+ final JsonObject field2 = fields2.get(0).getAsJsonObject();
+ assertThat(field2.get("key").getAsString(), is("description"));
+ assertThat(field2.get("type").getAsString(), is("string"));
+ assertThat(field2.get("value").getAsString(), is(FINISHED_PROCESSING_VIDEO));
+
+ assertThat(span.get("processID").getAsString(), is("p1"));
+ assertThat(span.get("warnings").getAsJsonNull(), is(JsonNull.INSTANCE));
+
+ final JsonObject processes = trace.get("processes").getAsJsonObject();
+ assertThat(processes.size(), is(1));
+ final JsonObject p1 = processes.get("p1").getAsJsonObject();
+ assertThat(p1.get("serviceName").getAsString(), is(SERVICE_NAME));
+ assertThat(p1.get("tags").getAsJsonArray().size(), is(0));
+ assertThat(trace.get("warnings").getAsJsonNull(), is(JsonNull.INSTANCE));
+ } finally {
+ jaeger.destroy();
+ }
+ }
+
+ private static boolean dockerIsInstalledAndExecutable() {
+ final TreeTraverser<File> traverser = Files.fileTreeTraverser();
+ for (final String pathPart : Splitter.on(File.pathSeparator).split(System.getenv("PATH"))) {
+ final File file = new File(pathPart);
+ if (isDocker(file) && file.canExecute()) {
+ return true;
+ } else if (file.isDirectory()) {
+ for (final File child : traverser.children(file)) {
+ if (isDocker(child) && child.canExecute()) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private static boolean isDocker(final File file) {
+ return file.isFile() && file.getName().toLowerCase().equals("docker");
+ }
+}
diff --git a/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerTest.java b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerTest.java
new file mode 100644
index 00000000..bb1acb36
--- /dev/null
+++ b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerTest.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2018, OpenCensus 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.opencensus.exporter.trace.jaeger;
+
+import static java.util.Collections.singletonList;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.hamcrest.Matchers.hasItems;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.uber.jaeger.senders.HttpSender;
+import com.uber.jaeger.thriftjava.Log;
+import com.uber.jaeger.thriftjava.Process;
+import com.uber.jaeger.thriftjava.Span;
+import com.uber.jaeger.thriftjava.SpanRef;
+import com.uber.jaeger.thriftjava.SpanRefType;
+import com.uber.jaeger.thriftjava.Tag;
+import com.uber.jaeger.thriftjava.TagType;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.export.SpanData;
+import java.util.List;
+import org.apache.thrift.TException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class JaegerExporterHandlerTest {
+ private static final byte FF = (byte) 0xFF;
+
+ private final HttpSender mockSender = mock(HttpSender.class);
+ private final Process process = new Process("test");
+ private final JaegerExporterHandler handler = new JaegerExporterHandler(mockSender, process);
+
+ @Captor private ArgumentCaptor<List<Span>> captor;
+
+ @Test
+ public void exportShouldConvertFromSpanDataToJaegerThriftSpan() throws TException {
+ final long startTime = 1519629870001L;
+ final long endTime = 1519630148002L;
+ final SpanData spanData =
+ SpanData.create(
+ sampleSpanContext(),
+ SpanId.fromBytes(new byte[] {(byte) 0x7F, FF, FF, FF, FF, FF, FF, FF}),
+ true,
+ "test",
+ Timestamp.fromMillis(startTime),
+ SpanData.Attributes.create(sampleAttributes(), 0),
+ SpanData.TimedEvents.create(singletonList(sampleAnnotation()), 0),
+ SpanData.TimedEvents.create(singletonList(sampleMessageEvent()), 0),
+ SpanData.Links.create(sampleLinks(), 0),
+ 0,
+ Status.OK,
+ Timestamp.fromMillis(endTime));
+
+ handler.export(singletonList(spanData));
+
+ verify(mockSender).send(eq(process), captor.capture());
+ List<Span> spans = captor.getValue();
+
+ assertThat(spans.size(), is(1));
+ Span span = spans.get(0);
+
+ assertThat(span.operationName, is("test"));
+ assertThat(span.spanId, is(256L));
+ assertThat(span.traceIdHigh, is(-72057594037927936L));
+ assertThat(span.traceIdLow, is(1L));
+ assertThat(span.parentSpanId, is(Long.MAX_VALUE));
+ assertThat(span.flags, is(1));
+ assertThat(span.startTime, is(MILLISECONDS.toMicros(startTime)));
+ assertThat(span.duration, is(MILLISECONDS.toMicros(endTime - startTime)));
+
+ assertThat(span.tags.size(), is(3));
+ assertThat(
+ span.tags,
+ hasItems(
+ new Tag("BOOL", TagType.BOOL).setVBool(false),
+ new Tag("LONG", TagType.LONG).setVLong(Long.MAX_VALUE),
+ new Tag("STRING", TagType.STRING)
+ .setVStr(
+ "Judge of a man by his questions rather than by his answers. -- Voltaire")));
+
+ assertThat(span.logs.size(), is(1));
+ Log log = span.logs.get(0);
+ assertThat(log.timestamp, is(1519629872987654L));
+ assertThat(log.fields.size(), is(4));
+ assertThat(
+ log.fields,
+ hasItems(
+ new Tag("description", TagType.STRING).setVStr("annotation #1"),
+ new Tag("bool", TagType.BOOL).setVBool(true),
+ new Tag("long", TagType.LONG).setVLong(1337L),
+ new Tag("string", TagType.STRING)
+ .setVStr("Kind words do not cost much. Yet they accomplish much. -- Pascal")));
+
+ assertThat(span.references.size(), is(1));
+ SpanRef reference = span.references.get(0);
+ assertThat(reference.traceIdHigh, is(-1L));
+ assertThat(reference.traceIdLow, is(-256L));
+ assertThat(reference.spanId, is(512L));
+ assertThat(reference.refType, is(SpanRefType.CHILD_OF));
+ }
+
+ private static SpanContext sampleSpanContext() {
+ return SpanContext.create(
+ TraceId.fromBytes(new byte[] {FF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}),
+ SpanId.fromBytes(new byte[] {0, 0, 0, 0, 0, 0, 1, 0}),
+ TraceOptions.builder().setIsSampled(true).build());
+ }
+
+ private static ImmutableMap<String, AttributeValue> sampleAttributes() {
+ return ImmutableMap.of(
+ "BOOL", AttributeValue.booleanAttributeValue(false),
+ "LONG", AttributeValue.longAttributeValue(Long.MAX_VALUE),
+ "STRING",
+ AttributeValue.stringAttributeValue(
+ "Judge of a man by his questions rather than by his answers. -- Voltaire"));
+ }
+
+ private static SpanData.TimedEvent<Annotation> sampleAnnotation() {
+ return SpanData.TimedEvent.create(
+ Timestamp.create(1519629872L, 987654321),
+ Annotation.fromDescriptionAndAttributes(
+ "annotation #1",
+ ImmutableMap.of(
+ "bool", AttributeValue.booleanAttributeValue(true),
+ "long", AttributeValue.longAttributeValue(1337L),
+ "string",
+ AttributeValue.stringAttributeValue(
+ "Kind words do not cost much. Yet they accomplish much. -- Pascal"))));
+ }
+
+ private static SpanData.TimedEvent<MessageEvent> sampleMessageEvent() {
+ return SpanData.TimedEvent.create(
+ Timestamp.create(1519629871L, 123456789),
+ MessageEvent.builder(MessageEvent.Type.SENT, 42L).build());
+ }
+
+ private static List<Link> sampleLinks() {
+ return Lists.newArrayList(
+ Link.fromSpanContext(
+ SpanContext.create(
+ TraceId.fromBytes(
+ new byte[] {FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, 0}),
+ SpanId.fromBytes(new byte[] {0, 0, 0, 0, 0, 0, 2, 0}),
+ TraceOptions.builder().setIsSampled(false).build()),
+ Link.Type.CHILD_LINKED_SPAN,
+ ImmutableMap.of(
+ "Bool", AttributeValue.booleanAttributeValue(true),
+ "Long", AttributeValue.longAttributeValue(299792458L),
+ "String",
+ AttributeValue.stringAttributeValue(
+ "Man is condemned to be free; because once thrown into the world, "
+ + "he is responsible for everything he does. -- Sartre"))));
+ }
+}
diff --git a/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporterTest.java b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporterTest.java
new file mode 100644
index 00000000..c00b0133
--- /dev/null
+++ b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporterTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018, OpenCensus 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.opencensus.exporter.trace.jaeger;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+
+import io.opencensus.trace.export.SpanExporter;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(JUnit4.class)
+public class JaegerTraceExporterTest {
+ @Mock private SpanExporter spanExporter;
+
+ @Mock private SpanExporter.Handler handler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void registerUnregisterJaegerExporter() {
+ JaegerTraceExporter.register(spanExporter, handler);
+ verify(spanExporter)
+ .registerHandler(
+ eq("io.opencensus.exporter.trace.jaeger.JaegerTraceExporter"), same(handler));
+ JaegerTraceExporter.unregister(spanExporter);
+ verify(spanExporter)
+ .unregisterHandler(eq("io.opencensus.exporter.trace.jaeger.JaegerTraceExporter"));
+ }
+}