diff options
| author | Marc Carré <marccarre@users.noreply.github.com> | 2018-03-16 01:03:49 +0000 |
|---|---|---|
| committer | Hailong Wen <youxiabsyw@gmail.com> | 2018-03-15 18:03:49 -0700 |
| commit | 8190805f302dfe4282a69b1c077dc6097abebdd2 (patch) | |
| tree | b545366faad899c060b48964f7221cb3504b5890 /exporters | |
| parent | e43d9c496557a388bffa61e33f39c305d6d86740 (diff) | |
| download | platform_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')
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")); + } +} |
