aboutsummaryrefslogtreecommitdiff
path: root/exporters/trace
diff options
context:
space:
mode:
Diffstat (limited to 'exporters/trace')
-rw-r--r--exporters/trace/instana/README.md73
-rw-r--r--exporters/trace/instana/build.gradle16
-rw-r--r--exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaExporterHandler.java235
-rw-r--r--exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaTraceExporter.java107
-rw-r--r--exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaExporterHandlerTest.java178
-rw-r--r--exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaTraceExporterTest.java54
-rw-r--r--exporters/trace/jaeger/README.md90
-rw-r--r--exporters/trace/jaeger/build.gradle37
-rw-r--r--exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandler.java321
-rw-r--r--exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporter.java136
-rw-r--r--exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerIntegrationTest.java226
-rw-r--r--exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerTest.java182
-rw-r--r--exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporterTest.java52
-rw-r--r--exporters/trace/logging/README.md57
-rw-r--r--exporters/trace/logging/build.gradle11
-rw-r--r--exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingExporter.java81
-rw-r--r--exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingTraceExporter.java101
-rw-r--r--exporters/trace/logging/src/test/java/io/opencensus/exporter/trace/logging/LoggingTraceExporterTest.java53
-rw-r--r--exporters/trace/ocagent/README.md48
-rw-r--r--exporters/trace/ocagent/build.gradle21
-rw-r--r--exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtils.java184
-rw-r--r--exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporter.java126
-rw-r--r--exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfiguration.java155
-rw-r--r--exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterHandler.java62
-rw-r--r--exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtils.java390
-rw-r--r--exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/package-info.java29
-rw-r--r--exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImpl.java169
-rw-r--r--exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImplTest.java109
-rw-r--r--exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtilsTest.java122
-rw-r--r--exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfigurationTest.java58
-rw-r--r--exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterTest.java54
-rw-r--r--exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtilsTest.java357
-rw-r--r--exporters/trace/stackdriver/README.md127
-rw-r--r--exporters/trace/stackdriver/build.gradle31
-rw-r--r--exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverExporter.java148
-rw-r--r--exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfiguration.java118
-rw-r--r--exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporter.java141
-rw-r--r--exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandler.java501
-rw-r--r--exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfigurationTest.java54
-rw-r--r--exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporterTest.java53
-rw-r--r--exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerExportTest.java64
-rw-r--r--exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerProtoTest.java489
-rw-r--r--exporters/trace/zipkin/README.md82
-rw-r--r--exporters/trace/zipkin/build.gradle18
-rw-r--r--exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporter.java104
-rw-r--r--exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandler.java215
-rw-r--r--exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporter.java124
-rw-r--r--exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandlerTest.java238
-rw-r--r--exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporterTest.java53
49 files changed, 6454 insertions, 0 deletions
diff --git a/exporters/trace/instana/README.md b/exporters/trace/instana/README.md
new file mode 100644
index 00000000..22ace227
--- /dev/null
+++ b/exporters/trace/instana/README.md
@@ -0,0 +1,73 @@
+# OpenCensus Instana Trace Exporter
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Instana Trace Exporter* is a trace exporter that exports
+data to Instana. [Instana](http://www.instana.com/) is a distributed
+tracing system.
+
+## Quickstart
+
+### Prerequisites
+
+[Instana](http://www.instana.com/) forwards traces exported by applications
+instrumented with Census to its backend using the Instana agent processes as proxy.
+If the agent is used on the same host as Census, please take care to deactivate
+automatic tracing.
+
+
+### Hello Stan
+
+#### 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.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-trace-instana</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-trace-instana:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### Register the exporter
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) throws Exception {
+ InstanaTraceExporter.createAndRegister("http://localhost:42699/com.instana.plugin.generic.trace");
+ // ...
+ }
+}
+```
+
+#### 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-instana/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-instana
diff --git a/exporters/trace/instana/build.gradle b/exporters/trace/instana/build.gradle
new file mode 100644
index 00000000..028bc208
--- /dev/null
+++ b/exporters/trace/instana/build.gradle
@@ -0,0 +1,16 @@
+description = 'OpenCensus Trace Instana Exporter'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.6
+ it.targetCompatibility = 1.6
+}
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.guava
+
+ testCompile project(':opencensus-api')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaExporterHandler.java b/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaExporterHandler.java
new file mode 100644
index 00000000..649a026f
--- /dev/null
+++ b/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaExporterHandler.java
@@ -0,0 +1,235 @@
+/*
+ * 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.instana;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.io.BaseEncoding;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Scope;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+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.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/*
+ * Exports to an Instana agent acting as proxy to the Instana backend (and handling authentication)
+ * Uses the Trace SDK documented:
+ * https://github.com/instana/instana-java-sdk#instana-trace-webservice
+ *
+ * Currently does a blocking export using HttpUrlConnection.
+ * Also uses a StringBuilder to build JSON.
+ * Both can be improved should 3rd party library usage not be a concern.
+ *
+ * Major TODO is the limitation of Instana to only suport 64bit trace ids, which will be resolved.
+ * Until then it is crossing fingers and treating it as 50% sampler :).
+ */
+final class InstanaExporterHandler extends SpanExporter.Handler {
+
+ private static final Tracer tracer = Tracing.getTracer();
+ private static final Sampler probabilitySpampler = Samplers.probabilitySampler(0.0001);
+ private final URL agentEndpoint;
+
+ InstanaExporterHandler(URL agentEndpoint) {
+ this.agentEndpoint = agentEndpoint;
+ }
+
+ private static String encodeTraceId(TraceId traceId) {
+ return BaseEncoding.base16().lowerCase().encode(traceId.getBytes(), 0, 8);
+ }
+
+ private static String encodeSpanId(SpanId spanId) {
+ return BaseEncoding.base16().lowerCase().encode(spanId.getBytes());
+ }
+
+ private static String toSpanName(SpanData spanData) {
+ return spanData.getName();
+ }
+
+ private static String toSpanType(SpanData spanData) {
+ if (spanData.getKind() == Kind.SERVER
+ || (spanData.getKind() == null
+ && (spanData.getParentSpanId() == null
+ || Boolean.TRUE.equals(spanData.getHasRemoteParent())))) {
+ return "ENTRY";
+ }
+
+ // This is a hack because the Span API did not have SpanKind.
+ if (spanData.getKind() == Kind.CLIENT
+ || (spanData.getKind() == null && spanData.getName().startsWith("Sent."))) {
+ return "EXIT";
+ }
+
+ return "INTERMEDIATE";
+ }
+
+ private static long toMillis(Timestamp timestamp) {
+ return SECONDS.toMillis(timestamp.getSeconds()) + NANOSECONDS.toMillis(timestamp.getNanos());
+ }
+
+ private static long toMillis(Timestamp start, Timestamp end) {
+ Duration duration = end.subtractTimestamp(start);
+ return SECONDS.toMillis(duration.getSeconds()) + NANOSECONDS.toMillis(duration.getNanos());
+ }
+
+ // The return type needs to be nullable when this function is used as an argument to 'match' in
+ // attributeValueToString, because 'match' doesn't allow covariant return types.
+ private static final Function<Object, /*@Nullable*/ String> returnToString =
+ Functions.returnToString();
+
+ @javax.annotation.Nullable
+ private static String attributeValueToString(AttributeValue attributeValue) {
+ return attributeValue.match(
+ returnToString,
+ returnToString,
+ returnToString,
+ returnToString,
+ Functions.</*@Nullable*/ String>returnNull());
+ }
+
+ static String convertToJson(Collection<SpanData> spanDataList) {
+ StringBuilder sb = new StringBuilder();
+ sb.append('[');
+ for (final SpanData span : spanDataList) {
+ final SpanContext spanContext = span.getContext();
+ final SpanId parentSpanId = span.getParentSpanId();
+ final Timestamp startTimestamp = span.getStartTimestamp();
+ final Timestamp endTimestamp = span.getEndTimestamp();
+ final Status status = span.getStatus();
+ if (status == null || endTimestamp == null) {
+ continue;
+ }
+ if (sb.length() > 1) {
+ sb.append(',');
+ }
+ sb.append('{');
+ sb.append("\"spanId\":\"").append(encodeSpanId(spanContext.getSpanId())).append("\",");
+ sb.append("\"traceId\":\"").append(encodeTraceId(spanContext.getTraceId())).append("\",");
+ if (parentSpanId != null) {
+ sb.append("\"parentId\":\"").append(encodeSpanId(parentSpanId)).append("\",");
+ }
+ sb.append("\"timestamp\":").append(toMillis(startTimestamp)).append(',');
+ sb.append("\"duration\":").append(toMillis(startTimestamp, endTimestamp)).append(',');
+ sb.append("\"name\":\"").append(toSpanName(span)).append("\",");
+ sb.append("\"type\":\"").append(toSpanType(span)).append('"');
+ if (!status.isOk()) {
+ sb.append(",\"error\":").append("true");
+ }
+ Map<String, AttributeValue> attributeMap = span.getAttributes().getAttributeMap();
+ if (attributeMap.size() > 0) {
+ StringBuilder dataSb = new StringBuilder();
+ dataSb.append('{');
+ for (Entry<String, AttributeValue> entry : attributeMap.entrySet()) {
+ if (dataSb.length() > 1) {
+ dataSb.append(',');
+ }
+ dataSb
+ .append("\"")
+ .append(entry.getKey())
+ .append("\":\"")
+ .append(attributeValueToString(entry.getValue()))
+ .append("\"");
+ }
+ dataSb.append('}');
+
+ sb.append(",\"data\":").append(dataSb);
+ }
+ sb.append('}');
+ }
+ sb.append(']');
+ return sb.toString();
+ }
+
+ @Override
+ public void export(Collection<SpanData> spanDataList) {
+ // Start a new span with explicit 1/10000 sampling probability to avoid the case when user
+ // sets the default sampler to always sample and we get the gRPC span of the instana
+ // export call always sampled and go to an infinite loop.
+ Scope scope =
+ tracer.spanBuilder("ExportInstanaTraces").setSampler(probabilitySpampler).startScopedSpan();
+ try {
+ String json = convertToJson(spanDataList);
+
+ OutputStream outputStream = null;
+ InputStream inputStream = null;
+ try {
+ HttpURLConnection connection = (HttpURLConnection) agentEndpoint.openConnection();
+ connection.setRequestMethod("POST");
+ connection.setDoOutput(true);
+ outputStream = connection.getOutputStream();
+ outputStream.write(json.getBytes(Charset.defaultCharset()));
+ outputStream.flush();
+ inputStream = connection.getInputStream();
+ if (connection.getResponseCode() != 200) {
+ tracer
+ .getCurrentSpan()
+ .setStatus(
+ Status.UNKNOWN.withDescription("Response " + connection.getResponseCode()));
+ }
+ } catch (IOException e) {
+ tracer
+ .getCurrentSpan()
+ .setStatus(
+ Status.UNKNOWN.withDescription(
+ e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
+ // dropping span batch
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ if (outputStream != null) {
+ try {
+ outputStream.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+ } finally {
+ scope.close();
+ }
+ }
+}
diff --git a/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaTraceExporter.java b/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaTraceExporter.java
new file mode 100644
index 00000000..da2ce354
--- /dev/null
+++ b/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaTraceExporter.java
@@ -0,0 +1,107 @@
+/*
+ * 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.instana;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import java.net.MalformedURLException;
+import java.net.URL;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Instana.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * InstanaTraceExporter.createAndRegister("http://localhost:42699/com.instana.plugin.generic.trace");
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @since 0.12
+ */
+public final class InstanaTraceExporter {
+
+ private static final String REGISTER_NAME = InstanaTraceExporter.class.getName();
+ private static final Object monitor = new Object();
+
+ @GuardedBy("monitor")
+ @Nullable
+ private static Handler handler = null;
+
+ private InstanaTraceExporter() {}
+
+ /**
+ * Creates and registers the Instana Trace exporter to the OpenCensus library. Only one Instana
+ * exporter can be registered at any point.
+ *
+ * @param agentEndpoint Ex http://localhost:42699/com.instana.plugin.generic.trace
+ * @throws MalformedURLException if the agentEndpoint is not a valid http url.
+ * @throws IllegalStateException if a Instana exporter is already registered.
+ * @since 0.12
+ */
+ public static void createAndRegister(String agentEndpoint) throws MalformedURLException {
+ synchronized (monitor) {
+ checkState(handler == null, "Instana exporter is already registered.");
+ Handler newHandler = new InstanaExporterHandler(new URL(agentEndpoint));
+ handler = newHandler;
+ register(Tracing.getExportComponent().getSpanExporter(), newHandler);
+ }
+ }
+
+ /**
+ * Registers the {@code InstanaTraceExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(SpanExporter spanExporter, Handler handler) {
+ spanExporter.registerHandler(REGISTER_NAME, handler);
+ }
+
+ /**
+ * Unregisters the Instana Trace exporter from the OpenCensus library.
+ *
+ * @throws IllegalStateException if a Instana exporter is not registered.
+ * @since 0.12
+ */
+ public static void unregister() {
+ synchronized (monitor) {
+ checkState(handler != null, "Instana exporter is not registered.");
+ unregister(Tracing.getExportComponent().getSpanExporter());
+ handler = null;
+ }
+ }
+
+ /**
+ * Unregisters the {@code InstanaTraceExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(SpanExporter spanExporter) {
+ spanExporter.unregisterHandler(REGISTER_NAME);
+ }
+}
diff --git a/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaExporterHandlerTest.java b/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaExporterHandlerTest.java
new file mode 100644
index 00000000..3b5e119e
--- /dev/null
+++ b/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaExporterHandlerTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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.instana;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+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.MessageEvent.Type;
+import io.opencensus.trace.Span.Kind;
+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 io.opencensus.trace.export.SpanData.Attributes;
+import io.opencensus.trace.export.SpanData.Links;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link InstanaExporterHandler}. */
+@RunWith(JUnit4.class)
+public class InstanaExporterHandlerTest {
+ private static final String TRACE_ID = "d239036e7d5cec116b562147388b35bf";
+ private static final String SPAN_ID = "9cc1e3049173be09";
+ private static final String PARENT_SPAN_ID = "8b03ab423da481c5";
+ private static final Map<String, AttributeValue> attributes =
+ ImmutableMap.of("http.url", AttributeValue.stringAttributeValue("http://localhost/foo"));
+ private static final List<TimedEvent<Annotation>> annotations = Collections.emptyList();
+ private static final List<TimedEvent<MessageEvent>> messageEvents =
+ ImmutableList.of(
+ TimedEvent.create(
+ Timestamp.create(1505855799, 433901068),
+ MessageEvent.builder(Type.RECEIVED, 0).setCompressedMessageSize(7).build()),
+ TimedEvent.create(
+ Timestamp.create(1505855799, 459486280),
+ MessageEvent.builder(Type.SENT, 0).setCompressedMessageSize(13).build()));
+
+ @Test
+ public void generateSpan_NoKindAndRemoteParent() {
+ SpanData data =
+ SpanData.create(
+ SpanContext.create(
+ TraceId.fromLowerBase16(TRACE_ID),
+ SpanId.fromLowerBase16(SPAN_ID),
+ TraceOptions.builder().setIsSampled(true).build()),
+ SpanId.fromLowerBase16(PARENT_SPAN_ID),
+ true, /* hasRemoteParent */
+ "SpanName", /* name */
+ null, /* kind */
+ Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+ Attributes.create(attributes, 0 /* droppedAttributesCount */),
+ TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+ TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+ Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+ null, /* childSpanCount */
+ Status.OK,
+ Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+ assertThat(InstanaExporterHandler.convertToJson(Collections.singletonList(data)))
+ .isEqualTo(
+ "["
+ + "{"
+ + "\"spanId\":\"9cc1e3049173be09\","
+ + "\"traceId\":\"d239036e7d5cec11\","
+ + "\"parentId\":\"8b03ab423da481c5\","
+ + "\"timestamp\":1505855794194,"
+ + "\"duration\":5271,"
+ + "\"name\":\"SpanName\","
+ + "\"type\":\"ENTRY\","
+ + "\"data\":"
+ + "{\"http.url\":\"http://localhost/foo\"}"
+ + "}"
+ + "]");
+ }
+
+ @Test
+ public void generateSpan_ServerKind() {
+ SpanData data =
+ SpanData.create(
+ SpanContext.create(
+ TraceId.fromLowerBase16(TRACE_ID),
+ SpanId.fromLowerBase16(SPAN_ID),
+ TraceOptions.builder().setIsSampled(true).build()),
+ SpanId.fromLowerBase16(PARENT_SPAN_ID),
+ true, /* hasRemoteParent */
+ "SpanName", /* name */
+ Kind.SERVER, /* kind */
+ Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+ Attributes.create(attributes, 0 /* droppedAttributesCount */),
+ TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+ TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+ Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+ null, /* childSpanCount */
+ Status.OK,
+ Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+ assertThat(InstanaExporterHandler.convertToJson(Collections.singletonList(data)))
+ .isEqualTo(
+ "["
+ + "{"
+ + "\"spanId\":\"9cc1e3049173be09\","
+ + "\"traceId\":\"d239036e7d5cec11\","
+ + "\"parentId\":\"8b03ab423da481c5\","
+ + "\"timestamp\":1505855794194,"
+ + "\"duration\":5271,"
+ + "\"name\":\"SpanName\","
+ + "\"type\":\"ENTRY\","
+ + "\"data\":"
+ + "{\"http.url\":\"http://localhost/foo\"}"
+ + "}"
+ + "]");
+ }
+
+ @Test
+ public void generateSpan_ClientKind() {
+ SpanData data =
+ SpanData.create(
+ SpanContext.create(
+ TraceId.fromLowerBase16(TRACE_ID),
+ SpanId.fromLowerBase16(SPAN_ID),
+ TraceOptions.builder().setIsSampled(true).build()),
+ SpanId.fromLowerBase16(PARENT_SPAN_ID),
+ true, /* hasRemoteParent */
+ "SpanName", /* name */
+ Kind.CLIENT, /* kind */
+ Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+ Attributes.create(attributes, 0 /* droppedAttributesCount */),
+ TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+ TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+ Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+ null, /* childSpanCount */
+ Status.OK,
+ Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+ assertThat(InstanaExporterHandler.convertToJson(Collections.singletonList(data)))
+ .isEqualTo(
+ "["
+ + "{"
+ + "\"spanId\":\"9cc1e3049173be09\","
+ + "\"traceId\":\"d239036e7d5cec11\","
+ + "\"parentId\":\"8b03ab423da481c5\","
+ + "\"timestamp\":1505855794194,"
+ + "\"duration\":5271,"
+ + "\"name\":\"SpanName\","
+ + "\"type\":\"EXIT\","
+ + "\"data\":"
+ + "{\"http.url\":\"http://localhost/foo\"}"
+ + "}"
+ + "]");
+ }
+}
diff --git a/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaTraceExporterTest.java b/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaTraceExporterTest.java
new file mode 100644
index 00000000..a4d03df3
--- /dev/null
+++ b/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaTraceExporterTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.instana;
+
+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 io.opencensus.trace.export.SpanExporter.Handler;
+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;
+
+/** Unit tests for {@link InstanaTraceExporter}. */
+@RunWith(JUnit4.class)
+public class InstanaTraceExporterTest {
+
+ @Mock private SpanExporter spanExporter;
+ @Mock private Handler handler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void registerUnregisterInstanaExporter() {
+ InstanaTraceExporter.register(spanExporter, handler);
+ verify(spanExporter)
+ .registerHandler(
+ eq("io.opencensus.exporter.trace.instana.InstanaTraceExporter"), same(handler));
+ InstanaTraceExporter.unregister(spanExporter);
+ verify(spanExporter)
+ .unregisterHandler(eq("io.opencensus.exporter.trace.instana.InstanaTraceExporter"));
+ }
+}
diff --git a/exporters/trace/jaeger/README.md b/exporters/trace/jaeger/README.md
new file mode 100644
index 00000000..7a5b68eb
--- /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.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-trace-jaeger</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-trace-jaeger:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### 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..04829aa4
--- /dev/null
+++ b/exporters/trace/jaeger/build.gradle
@@ -0,0 +1,37 @@
+description = 'OpenCensus Trace Jaeger Exporter'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.6
+ it.targetCompatibility = 1.6
+}
+
+// Docker tests require JDK 8+
+sourceSets {
+ test {
+ java {
+ if (!JavaVersion.current().isJava8Compatible()) {
+ exclude '**/JaegerExporterHandlerIntegrationTest.java'
+ }
+ }
+ }
+}
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.guava
+
+ compile(libraries.jaeger_reporter) {
+ // Prefer library version.
+ exclude group: 'com.google.guava', module: 'guava'
+ }
+
+ testCompile project(':opencensus-api'),
+ 'org.testcontainers:testcontainers:1.7.0',
+ 'com.google.http-client:google-http-client-gson:1.23.0'
+
+ // Unless linked to impl, spans will be blank and not exported during integration tests.
+ testRuntime project(':opencensus-impl')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@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..e0a16296
--- /dev/null
+++ b/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandler.java
@@ -0,0 +1,321 @@
+/*
+ * 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.exceptions.SenderException;
+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;
+
+@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 Double, Tag> doubleAttributeConverter =
+ new Function<Double, Tag>() {
+ @Override
+ public Tag apply(final Double value) {
+ final Tag tag = new Tag();
+ tag.setVType(TagType.DOUBLE);
+ tag.setVDouble(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 (SenderException 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 SenderException {
+ final List<Span> spans = spanDataToJaegerThriftSpans(spanDataList);
+ sender.send(process, spans);
+ }
+
+ private static String getMessageOrDefault(final SenderException 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,
+ doubleAttributeConverter,
+ 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..4890f01a
--- /dev/null
+++ b/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporter.java
@@ -0,0 +1,136 @@
+/*
+ * 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);
+ }
+ }
+
+ /**
+ * Creates and registers the Jaeger Trace exporter to the OpenCensus library using the provided
+ * HttpSender. Only one Jaeger exporter can be registered at any point.
+ *
+ * @param httpSender the pre-configured HttpSender to use with the exporter
+ * @param serviceName the local service name of the process.
+ * @throws IllegalStateException if a Jaeger exporter is already registered.
+ * @since 0.17
+ */
+ public static void createWithSender(final HttpSender httpSender, final String serviceName) {
+ synchronized (monitor) {
+ checkState(handler == null, "Jaeger exporter is already registered.");
+ final SpanExporter.Handler newHandler = newHandlerWithSender(httpSender, 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);
+ }
+
+ private static SpanExporter.Handler newHandlerWithSender(
+ final HttpSender sender, final String serviceName) {
+ 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..9d6a7976
--- /dev/null
+++ b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerIntegrationTest.java
@@ -0,0 +1,226 @@
+/*
+ * 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.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static java.lang.String.format;
+import static java.lang.System.currentTimeMillis;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.javanet.NetHttpTransport;
+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.IOException;
+import java.util.Random;
+import org.junit.AfterClass;
+import org.junit.AssumptionViolatedException;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
+
+public class JaegerExporterHandlerIntegrationTest {
+ private static final String JAEGER_IMAGE = "jaegertracing/all-in-one:1.3";
+ private static final int JAEGER_HTTP_PORT = 16686;
+ private static final int JAEGER_HTTP_PORT_THRIFT = 14268;
+ 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 =
+ LoggerFactory.getLogger(JaegerExporterHandlerIntegrationTest.class);
+
+ private final HttpRequestFactory httpRequestFactory =
+ new NetHttpTransport().createRequestFactory();
+
+ private static GenericContainer<?> container;
+
+ /** Starts a docker container optionally. For example, skips if Docker is unavailable. */
+ @SuppressWarnings("rawtypes")
+ @BeforeClass
+ public static void startContainer() {
+ try {
+ container =
+ new GenericContainer(JAEGER_IMAGE)
+ .withExposedPorts(JAEGER_HTTP_PORT, JAEGER_HTTP_PORT_THRIFT)
+ .waitingFor(new HttpWaitStrategy());
+ container.start();
+ } catch (RuntimeException e) {
+ throw new AssumptionViolatedException("could not start docker container", e);
+ }
+ }
+
+ @AfterClass
+ public static void stopContainer() {
+ if (container != null) {
+ container.stop();
+ }
+ }
+
+ @Before
+ public void before() {
+ JaegerTraceExporter.createAndRegister(thriftTracesEndpoint(), SERVICE_NAME);
+ }
+
+ @Test
+ public void exportToJaeger() throws InterruptedException, IOException {
+ Tracer tracer = Tracing.getTracer();
+ final long startTimeInMillis = currentTimeMillis();
+
+ SpanBuilder spanBuilder =
+ tracer.spanBuilder(SPAN_NAME).setRecordEvents(true).setSampler(Samplers.alwaysSample());
+ int spanDurationInMillis = new Random().nextInt(10) + 1;
+
+ Scope scopedSpan = spanBuilder.startScopedSpan();
+ try {
+ tracer.getCurrentSpan().addAnnotation(START_PROCESSING_VIDEO);
+ Thread.sleep(spanDurationInMillis); // Fake work.
+ 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.error(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.
+ long timeWaitingForSpansToBeExportedInMillis = 5100L;
+ Thread.sleep(timeWaitingForSpansToBeExportedInMillis);
+ JaegerTraceExporter.unregister();
+ final long endTimeInMillis = currentTimeMillis();
+
+ // Get traces recorded by Jaeger:
+ HttpRequest request =
+ httpRequestFactory.buildGetRequest(new GenericUrl(tracesForServiceEndpoint(SERVICE_NAME)));
+ HttpResponse response = request.execute();
+ String body = response.parseAsString();
+ assertWithMessage("Response was: " + body).that(response.getStatusCode()).isEqualTo(200);
+
+ JsonObject result = new JsonParser().parse(body).getAsJsonObject();
+ // Pretty-print for debugging purposes:
+ logger.debug(new GsonBuilder().setPrettyPrinting().create().toJson(result));
+
+ assertThat(result).isNotNull();
+ assertThat(result.get("total").getAsInt()).isEqualTo(0);
+ assertThat(result.get("limit").getAsInt()).isEqualTo(0);
+ assertThat(result.get("offset").getAsInt()).isEqualTo(0);
+ assertThat(result.get("errors").getAsJsonNull()).isEqualTo(JsonNull.INSTANCE);
+ JsonArray data = result.get("data").getAsJsonArray();
+ assertThat(data).isNotNull();
+ assertThat(data.size()).isEqualTo(1);
+ JsonObject trace = data.get(0).getAsJsonObject();
+ assertThat(trace).isNotNull();
+ assertThat(trace.get("traceID").getAsString()).matches("[a-z0-9]{1,32}");
+
+ JsonArray spans = trace.get("spans").getAsJsonArray();
+ assertThat(spans).isNotNull();
+ assertThat(spans.size()).isEqualTo(1);
+
+ JsonObject span = spans.get(0).getAsJsonObject();
+ assertThat(span).isNotNull();
+ assertThat(span.get("traceID").getAsString()).matches("[a-z0-9]{1,32}");
+ assertThat(span.get("spanID").getAsString()).matches("[a-z0-9]{1,16}");
+ assertThat(span.get("flags").getAsInt()).isEqualTo(1);
+ assertThat(span.get("operationName").getAsString()).isEqualTo(SPAN_NAME);
+ assertThat(span.get("references").getAsJsonArray()).isEmpty();
+ assertThat(span.get("startTime").getAsLong())
+ .isAtLeast(MILLISECONDS.toMicros(startTimeInMillis));
+ assertThat(span.get("startTime").getAsLong()).isAtMost(MILLISECONDS.toMicros(endTimeInMillis));
+ assertThat(span.get("duration").getAsLong())
+ .isAtLeast(MILLISECONDS.toMicros(spanDurationInMillis));
+ assertThat(span.get("duration").getAsLong())
+ .isAtMost(
+ MILLISECONDS.toMicros(spanDurationInMillis + timeWaitingForSpansToBeExportedInMillis));
+
+ JsonArray tags = span.get("tags").getAsJsonArray();
+ assertThat(tags.size()).isEqualTo(1);
+ JsonObject tag = tags.get(0).getAsJsonObject();
+ assertThat(tag.get("key").getAsString()).isEqualTo("foo");
+ assertThat(tag.get("type").getAsString()).isEqualTo("string");
+ assertThat(tag.get("value").getAsString()).isEqualTo("bar");
+
+ JsonArray logs = span.get("logs").getAsJsonArray();
+ assertThat(logs.size()).isEqualTo(2);
+
+ JsonObject log1 = logs.get(0).getAsJsonObject();
+ long ts1 = log1.get("timestamp").getAsLong();
+ assertThat(ts1).isAtLeast(MILLISECONDS.toMicros(startTimeInMillis));
+ assertThat(ts1).isAtMost(MILLISECONDS.toMicros(endTimeInMillis));
+ JsonArray fields1 = log1.get("fields").getAsJsonArray();
+ assertThat(fields1.size()).isEqualTo(1);
+ JsonObject field1 = fields1.get(0).getAsJsonObject();
+ assertThat(field1.get("key").getAsString()).isEqualTo("description");
+ assertThat(field1.get("type").getAsString()).isEqualTo("string");
+ assertThat(field1.get("value").getAsString()).isEqualTo(START_PROCESSING_VIDEO);
+
+ JsonObject log2 = logs.get(1).getAsJsonObject();
+ long ts2 = log2.get("timestamp").getAsLong();
+ assertThat(ts2).isAtLeast(MILLISECONDS.toMicros(startTimeInMillis));
+ assertThat(ts2).isAtMost(MILLISECONDS.toMicros(endTimeInMillis));
+ assertThat(ts2).isAtLeast(ts1);
+ JsonArray fields2 = log2.get("fields").getAsJsonArray();
+ assertThat(fields2.size()).isEqualTo(1);
+ JsonObject field2 = fields2.get(0).getAsJsonObject();
+ assertThat(field2.get("key").getAsString()).isEqualTo("description");
+ assertThat(field2.get("type").getAsString()).isEqualTo("string");
+ assertThat(field2.get("value").getAsString()).isEqualTo(FINISHED_PROCESSING_VIDEO);
+
+ assertThat(span.get("processID").getAsString()).isEqualTo("p1");
+ assertThat(span.get("warnings").getAsJsonNull()).isEqualTo(JsonNull.INSTANCE);
+
+ JsonObject processes = trace.get("processes").getAsJsonObject();
+ assertThat(processes.size()).isEqualTo(1);
+ JsonObject p1 = processes.get("p1").getAsJsonObject();
+ assertThat(p1.get("serviceName").getAsString()).isEqualTo(SERVICE_NAME);
+ assertThat(p1.get("tags").getAsJsonArray().size()).isEqualTo(0);
+ assertThat(trace.get("warnings").getAsJsonNull()).isEqualTo(JsonNull.INSTANCE);
+ }
+
+ private static String thriftTracesEndpoint() {
+ return format(
+ "http://%s:%s/api/traces",
+ container.getContainerIpAddress(), container.getMappedPort(JAEGER_HTTP_PORT_THRIFT));
+ }
+
+ private static String tracesForServiceEndpoint(String service) {
+ return format(
+ "http://%s:%s/api/traces?service=%s",
+ container.getContainerIpAddress(), container.getMappedPort(JAEGER_HTTP_PORT), service);
+ }
+}
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..f918f015
--- /dev/null
+++ b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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.truth.Truth.assertThat;
+import static java.util.Collections.singletonList;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+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.exceptions.SenderException;
+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.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 SenderException {
+ 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()).isEqualTo(1);
+ Span span = spans.get(0);
+
+ assertThat(span.operationName).isEqualTo("test");
+ assertThat(span.spanId).isEqualTo(256L);
+ assertThat(span.traceIdHigh).isEqualTo(-72057594037927936L);
+ assertThat(span.traceIdLow).isEqualTo(1L);
+ assertThat(span.parentSpanId).isEqualTo(Long.MAX_VALUE);
+ assertThat(span.flags).isEqualTo(1);
+ assertThat(span.startTime).isEqualTo(MILLISECONDS.toMicros(startTime));
+ assertThat(span.duration).isEqualTo(MILLISECONDS.toMicros(endTime - startTime));
+
+ assertThat(span.tags.size()).isEqualTo(3);
+ assertThat(span.tags)
+ .containsExactly(
+ 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()).isEqualTo(1);
+ Log log = span.logs.get(0);
+ assertThat(log.timestamp).isEqualTo(1519629872987654L);
+ assertThat(log.fields.size()).isEqualTo(4);
+ assertThat(log.fields)
+ .containsExactly(
+ 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()).isEqualTo(1);
+ SpanRef reference = span.references.get(0);
+ assertThat(reference.traceIdHigh).isEqualTo(-1L);
+ assertThat(reference.traceIdLow).isEqualTo(-256L);
+ assertThat(reference.spanId).isEqualTo(512L);
+ assertThat(reference.refType).isEqualTo(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"));
+ }
+}
diff --git a/exporters/trace/logging/README.md b/exporters/trace/logging/README.md
new file mode 100644
index 00000000..51f2566d
--- /dev/null
+++ b/exporters/trace/logging/README.md
@@ -0,0 +1,57 @@
+# OpenCensus Logging Trace Exporter
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Logging trace exporter* is a trace exporter that logs all data to the system log.
+
+## Quickstart
+
+### 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.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-trace-logging</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-trace-logging:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+### Register the exporter
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) throws Exception {
+ LoggingTraceExporter.register();
+ // ...
+ }
+}
+```
+
+[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-logging/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-logging
diff --git a/exporters/trace/logging/build.gradle b/exporters/trace/logging/build.gradle
new file mode 100644
index 00000000..a7fb0ff6
--- /dev/null
+++ b/exporters/trace/logging/build.gradle
@@ -0,0 +1,11 @@
+description = 'OpenCensus Trace Logging Exporter'
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.guava
+
+ testCompile project(':opencensus-api')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+} \ No newline at end of file
diff --git a/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingExporter.java b/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingExporter.java
new file mode 100644
index 00000000..46f01ffc
--- /dev/null
+++ b/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingExporter.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2017, 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.logging;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.export.SpanExporter;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An OpenCensus span exporter implementation which logs all data.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * LoggingExporter.register();
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @deprecated Deprecated due to inconsistent naming. Use {@link LoggingTraceExporter}.
+ * @since 0.6
+ */
+@ThreadSafe
+@Deprecated
+public final class LoggingExporter {
+ private LoggingExporter() {}
+
+ /**
+ * Registers the Logging exporter to the OpenCensus library.
+ *
+ * @since 0.6
+ */
+ public static void register() {
+ LoggingTraceExporter.register();
+ }
+
+ /**
+ * Registers the {@code LoggingHandler}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(SpanExporter spanExporter) {
+ LoggingTraceExporter.register(spanExporter);
+ }
+
+ /**
+ * Unregisters the Logging exporter from the OpenCensus library.
+ *
+ * @since 0.6
+ */
+ public static void unregister() {
+ LoggingTraceExporter.unregister();
+ }
+
+ /**
+ * Unregisters the {@code LoggingHandler}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(SpanExporter spanExporter) {
+ LoggingTraceExporter.unregister(spanExporter);
+ }
+}
diff --git a/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingTraceExporter.java b/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingTraceExporter.java
new file mode 100644
index 00000000..9267e201
--- /dev/null
+++ b/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingTraceExporter.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2017, 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.logging;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import java.util.Collection;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An OpenCensus span exporter implementation which logs all data.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * LoggingTraceExporter.register();
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @since 0.12
+ */
+@ThreadSafe
+public final class LoggingTraceExporter {
+ private static final Logger logger = Logger.getLogger(LoggingTraceExporter.class.getName());
+ private static final String REGISTER_NAME = LoggingTraceExporter.class.getName();
+ private static final LoggingExporterHandler HANDLER = new LoggingExporterHandler();
+
+ private LoggingTraceExporter() {}
+
+ /**
+ * Registers the Logging exporter to the OpenCensus library.
+ *
+ * @since 0.12
+ */
+ public static void register() {
+ register(Tracing.getExportComponent().getSpanExporter());
+ }
+
+ /**
+ * Registers the {@code LoggingHandler}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(SpanExporter spanExporter) {
+ spanExporter.registerHandler(REGISTER_NAME, HANDLER);
+ }
+
+ /**
+ * Unregisters the Logging exporter from the OpenCensus library.
+ *
+ * @since 0.12
+ */
+ public static void unregister() {
+ unregister(Tracing.getExportComponent().getSpanExporter());
+ }
+
+ /**
+ * Unregisters the {@code LoggingHandler}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(SpanExporter spanExporter) {
+ spanExporter.unregisterHandler(REGISTER_NAME);
+ }
+
+ @VisibleForTesting
+ static final class LoggingExporterHandler extends Handler {
+ @Override
+ public void export(Collection<SpanData> spanDataList) {
+ // TODO(bdrutu): Use JSON as a standard format for logging SpanData and define this to be
+ // compatible between languages.
+ for (SpanData spanData : spanDataList) {
+ logger.log(Level.INFO, spanData.toString());
+ }
+ }
+ }
+}
diff --git a/exporters/trace/logging/src/test/java/io/opencensus/exporter/trace/logging/LoggingTraceExporterTest.java b/exporters/trace/logging/src/test/java/io/opencensus/exporter/trace/logging/LoggingTraceExporterTest.java
new file mode 100644
index 00000000..c2b77e4e
--- /dev/null
+++ b/exporters/trace/logging/src/test/java/io/opencensus/exporter/trace/logging/LoggingTraceExporterTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017, 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.logging;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+
+import io.opencensus.exporter.trace.logging.LoggingTraceExporter.LoggingExporterHandler;
+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;
+
+/** Unit tests for {@link LoggingTraceExporter}. */
+@RunWith(JUnit4.class)
+public class LoggingTraceExporterTest {
+ @Mock private SpanExporter spanExporter;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void registerUnregisterLoggingService() {
+ LoggingTraceExporter.register(spanExporter);
+ verify(spanExporter)
+ .registerHandler(
+ eq("io.opencensus.exporter.trace.logging.LoggingTraceExporter"),
+ any(LoggingExporterHandler.class));
+ LoggingTraceExporter.unregister(spanExporter);
+ verify(spanExporter)
+ .unregisterHandler(eq("io.opencensus.exporter.trace.logging.LoggingTraceExporter"));
+ }
+}
diff --git a/exporters/trace/ocagent/README.md b/exporters/trace/ocagent/README.md
new file mode 100644
index 00000000..4f25bd6e
--- /dev/null
+++ b/exporters/trace/ocagent/README.md
@@ -0,0 +1,48 @@
+# OpenCensus Java OC-Agent Trace Exporter
+
+The *OpenCensus Java OC-Agent Trace Exporter* is the Java implementation of the OpenCensus Agent
+(OC-Agent) Trace Exporter.
+
+## Quickstart
+
+### 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.17.0</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-trace-ocagent</artifactId>
+ <version>0.17.0</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.17.0</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-api:0.17.0'
+compile 'io.opencensus:opencensus-exporter-trace-ocagent:0.17.0'
+runtime 'io.opencensus:opencensus-impl:0.17.0'
+```
+
+### Register the exporter
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) throws Exception {
+ OcAgentTraceExporter.createAndRegister();
+ // ...
+ }
+}
+```
diff --git a/exporters/trace/ocagent/build.gradle b/exporters/trace/ocagent/build.gradle
new file mode 100644
index 00000000..777c08d0
--- /dev/null
+++ b/exporters/trace/ocagent/build.gradle
@@ -0,0 +1,21 @@
+description = 'OpenCensus Java OC-Agent Trace Exporter'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.7
+ it.targetCompatibility = 1.7
+}
+
+dependencies {
+ compileOnly libraries.auto_value
+
+ compile project(':opencensus-api'),
+ project(':opencensus-contrib-monitored-resource-util'),
+ libraries.grpc_core,
+ libraries.grpc_netty,
+ libraries.grpc_stub,
+ libraries.opencensus_proto
+
+ testCompile project(':opencensus-api')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtils.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtils.java
new file mode 100644
index 00000000..65729803
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtils.java
@@ -0,0 +1,184 @@
+/*
+ * 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.ocagent;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.common.OpenCensusLibraryInformation;
+import io.opencensus.common.Timestamp;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResourceUtils;
+import io.opencensus.proto.agent.common.v1.LibraryInfo;
+import io.opencensus.proto.agent.common.v1.LibraryInfo.Language;
+import io.opencensus.proto.agent.common.v1.Node;
+import io.opencensus.proto.agent.common.v1.ProcessIdentifier;
+import io.opencensus.proto.agent.common.v1.ServiceInfo;
+import java.lang.management.ManagementFactory;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.SecureRandom;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/** Utilities for detecting and creating {@link Node}. */
+final class OcAgentNodeUtils {
+
+ // The current version of the OpenCensus OC-Agent Exporter.
+ @VisibleForTesting
+ static final String OC_AGENT_EXPORTER_VERSION = "0.17.0-SNAPSHOT"; // CURRENT_OPENCENSUS_VERSION
+
+ @VisibleForTesting static final String RESOURCE_TYPE_ATTRIBUTE_KEY = "OPENCENSUS_SOURCE_TYPE";
+ @VisibleForTesting static final String RESOURCE_LABEL_ATTRIBUTE_KEY = "OPENCENSUS_SOURCE_LABELS";
+
+ @Nullable
+ private static final MonitoredResource RESOURCE = MonitoredResourceUtils.getDefaultResource();
+
+ // Creates a Node with information from the OpenCensus library and environment variables.
+ static Node getNodeInfo(String serviceName) {
+ String jvmName = ManagementFactory.getRuntimeMXBean().getName();
+ Timestamp censusTimestamp = Timestamp.fromMillis(System.currentTimeMillis());
+ return Node.newBuilder()
+ .setIdentifier(getProcessIdentifier(jvmName, censusTimestamp))
+ .setLibraryInfo(getLibraryInfo(OpenCensusLibraryInformation.VERSION))
+ .setServiceInfo(getServiceInfo(serviceName))
+ .putAllAttributes(getAttributeMap(RESOURCE))
+ .build();
+ }
+
+ // Creates process identifier with the given JVM name and start time.
+ @VisibleForTesting
+ static ProcessIdentifier getProcessIdentifier(String jvmName, Timestamp censusTimestamp) {
+ String hostname;
+ int pid;
+ // jvmName should be something like '<pid>@<hostname>', at least in Oracle and OpenJdk JVMs
+ int delimiterIndex = jvmName.indexOf('@');
+ if (delimiterIndex < 1) {
+ // Not the expected format, generate a random number.
+ try {
+ hostname = InetAddress.getLocalHost().getHostName();
+ } catch (UnknownHostException e) {
+ hostname = "localhost";
+ }
+ // Generate a random number as the PID.
+ pid = new SecureRandom().nextInt();
+ } else {
+ hostname = jvmName.substring(delimiterIndex + 1, jvmName.length());
+ try {
+ pid = Integer.parseInt(jvmName.substring(0, delimiterIndex));
+ } catch (NumberFormatException e) {
+ // Generate a random number as the PID if format is unexpected.
+ pid = new SecureRandom().nextInt();
+ }
+ }
+
+ return ProcessIdentifier.newBuilder()
+ .setHostName(hostname)
+ .setPid(pid)
+ .setStartTimestamp(TraceProtoUtils.toTimestampProto(censusTimestamp))
+ .build();
+ }
+
+ // Creates library info with the given OpenCensus Java version.
+ @VisibleForTesting
+ static LibraryInfo getLibraryInfo(String currentOcJavaVersion) {
+ return LibraryInfo.newBuilder()
+ .setLanguage(Language.JAVA)
+ .setCoreLibraryVersion(currentOcJavaVersion)
+ .setExporterVersion(OC_AGENT_EXPORTER_VERSION)
+ .build();
+ }
+
+ // Creates service info with the given service name.
+ @VisibleForTesting
+ static ServiceInfo getServiceInfo(String serviceName) {
+ return ServiceInfo.newBuilder().setName(serviceName).build();
+ }
+
+ /*
+ * Creates an attribute map with the given MonitoredResource.
+ * If the given resource is not null, the attribute map contains exactly two entries:
+ *
+ * OPENCENSUS_SOURCE_TYPE:
+ * A string that describes the type of the resource prefixed by a domain namespace,
+ * e.g. “kubernetes.io/container”.
+ * OPENCENSUS_SOURCE_LABELS:
+ * A comma-separated list of labels describing the source in more detail,
+ * e.g. “key1=val1,key2=val2”. The allowed character set is appropriately constrained.
+ */
+ // TODO: update the resource attributes once we have an agreement on the resource specs:
+ // https://github.com/census-instrumentation/opencensus-specs/pull/162.
+ @VisibleForTesting
+ static Map<String, String> getAttributeMap(@Nullable MonitoredResource resource) {
+ if (resource == null) {
+ return Collections.emptyMap();
+ } else {
+ Map<String, String> resourceAttributes = new HashMap<String, String>();
+ resourceAttributes.put(RESOURCE_TYPE_ATTRIBUTE_KEY, resource.getResourceType().name());
+ resourceAttributes.put(RESOURCE_LABEL_ATTRIBUTE_KEY, getConcatenatedResourceLabels(resource));
+ return resourceAttributes;
+ }
+ }
+
+ // Encodes the attributes of MonitoredResource into a comma-separated list of labels.
+ // For example "aws_account=account1,instance_id=instance1,region=us-east-2".
+ private static String getConcatenatedResourceLabels(MonitoredResource resource) {
+ StringBuilder resourceLabels = new StringBuilder();
+ if (resource instanceof AwsEc2InstanceMonitoredResource) {
+ AwsEc2InstanceMonitoredResource awsEc2Resource = (AwsEc2InstanceMonitoredResource) resource;
+ putIntoBuilderIfHasValue(resourceLabels, "aws_account", awsEc2Resource.getAccount());
+ putIntoBuilderIfHasValue(resourceLabels, "instance_id", awsEc2Resource.getInstanceId());
+ putIntoBuilderIfHasValue(resourceLabels, "region", awsEc2Resource.getRegion());
+ } else if (resource instanceof GcpGceInstanceMonitoredResource) {
+ GcpGceInstanceMonitoredResource gceResource = (GcpGceInstanceMonitoredResource) resource;
+ putIntoBuilderIfHasValue(resourceLabels, "gcp_account", gceResource.getAccount());
+ putIntoBuilderIfHasValue(resourceLabels, "instance_id", gceResource.getInstanceId());
+ putIntoBuilderIfHasValue(resourceLabels, "zone", gceResource.getZone());
+ } else if (resource instanceof GcpGkeContainerMonitoredResource) {
+ GcpGkeContainerMonitoredResource gkeResource = (GcpGkeContainerMonitoredResource) resource;
+ putIntoBuilderIfHasValue(resourceLabels, "gcp_account", gkeResource.getAccount());
+ putIntoBuilderIfHasValue(resourceLabels, "instance_id", gkeResource.getInstanceId());
+ putIntoBuilderIfHasValue(resourceLabels, "location", gkeResource.getZone());
+ putIntoBuilderIfHasValue(resourceLabels, "namespace_name", gkeResource.getNamespaceId());
+ putIntoBuilderIfHasValue(resourceLabels, "cluster_name", gkeResource.getClusterName());
+ putIntoBuilderIfHasValue(resourceLabels, "container_name", gkeResource.getContainerName());
+ putIntoBuilderIfHasValue(resourceLabels, "pod_name", gkeResource.getPodId());
+ }
+ return resourceLabels.toString();
+ }
+
+ // If the given resourceValue is not empty, encodes resourceKey and resourceValue as
+ // "resourceKey:resourceValue" and puts it into the given StringBuilder. Otherwise skip the value.
+ private static void putIntoBuilderIfHasValue(
+ StringBuilder builder, String resourceKey, String resourceValue) {
+ if (resourceValue.isEmpty()) {
+ return;
+ }
+ if (!(builder.length() == 0)) {
+ // Appends the comma separator to the front, if the StringBuilder already has entries.
+ builder.append(',');
+ }
+ builder.append(resourceKey);
+ builder.append('=');
+ builder.append(resourceValue);
+ }
+
+ private OcAgentNodeUtils() {}
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporter.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporter.java
new file mode 100644
index 00000000..5c468ded
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporter.java
@@ -0,0 +1,126 @@
+/*
+ * 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.ocagent;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * The implementation of the OpenCensus Agent (OC-Agent) Trace Exporter.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * OcAgentTraceExporter.createAndRegister();
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @since 0.17
+ */
+@ThreadSafe
+public final class OcAgentTraceExporter {
+
+ private static final Object monitor = new Object();
+ private static final String REGISTER_NAME = OcAgentTraceExporter.class.getName();
+
+ @GuardedBy("monitor")
+ @Nullable
+ private static Handler handler = null;
+
+ private OcAgentTraceExporter() {}
+
+ /**
+ * Creates a {@code OcAgentTraceExporterHandler} with default configurations and registers it to
+ * the OpenCensus library.
+ *
+ * @since 0.17
+ */
+ public static void createAndRegister() {
+ synchronized (monitor) {
+ checkState(handler == null, "OC-Agent exporter is already registered.");
+ OcAgentTraceExporterHandler newHandler = new OcAgentTraceExporterHandler();
+ registerInternal(newHandler);
+ }
+ }
+
+ /**
+ * Creates a {@code OcAgentTraceExporterHandler} with the given configurations and registers it to
+ * the OpenCensus library.
+ *
+ * @param configuration the {@code OcAgentTraceExporterConfiguration}.
+ * @since 0.17
+ */
+ public static void createAndRegister(OcAgentTraceExporterConfiguration configuration) {
+ synchronized (monitor) {
+ checkState(handler == null, "OC-Agent exporter is already registered.");
+ OcAgentTraceExporterHandler newHandler =
+ new OcAgentTraceExporterHandler(
+ configuration.getEndPoint(),
+ configuration.getServiceName(),
+ configuration.getUseInsecure(),
+ configuration.getRetryInterval(),
+ configuration.getEnableConfig());
+ registerInternal(newHandler);
+ }
+ }
+
+ /**
+ * Registers the {@code OcAgentTraceExporterHandler}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(SpanExporter spanExporter, Handler handler) {
+ spanExporter.registerHandler(REGISTER_NAME, handler);
+ }
+
+ private static void registerInternal(Handler newHandler) {
+ synchronized (monitor) {
+ handler = newHandler;
+ register(Tracing.getExportComponent().getSpanExporter(), newHandler);
+ }
+ }
+
+ /**
+ * Unregisters the OC-Agent exporter from the OpenCensus library.
+ *
+ * @since 0.17
+ */
+ public static void unregister() {
+ unregister(Tracing.getExportComponent().getSpanExporter());
+ }
+
+ /**
+ * Unregisters the {@code OcAgentTraceExporterHandler}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(SpanExporter spanExporter) {
+ spanExporter.unregisterHandler(REGISTER_NAME);
+ }
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfiguration.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfiguration.java
new file mode 100644
index 00000000..c7bf1e95
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfiguration.java
@@ -0,0 +1,155 @@
+/*
+ * 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.ocagent;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Duration;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Configurations for {@link OcAgentTraceExporter}.
+ *
+ * @since 0.17
+ */
+@AutoValue
+@Immutable
+public abstract class OcAgentTraceExporterConfiguration {
+
+ OcAgentTraceExporterConfiguration() {}
+
+ /**
+ * Returns the end point of OC-Agent. The end point can be dns, ip:port, etc.
+ *
+ * @return the end point of OC-Agent.
+ * @since 0.17
+ */
+ @Nullable
+ public abstract String getEndPoint();
+
+ /**
+ * Returns whether to disable client transport security for the exporter's gRPC connection or not.
+ *
+ * @return whether to disable client transport security for the exporter's gRPC connection or not.
+ * @since 0.17
+ */
+ @Nullable
+ public abstract Boolean getUseInsecure();
+
+ /**
+ * Returns the service name to be used for this {@link OcAgentTraceExporter}.
+ *
+ * @return the service name.
+ * @since 0.17
+ */
+ @Nullable
+ public abstract String getServiceName();
+
+ /**
+ * Returns the retry time interval when trying to connect to Agent.
+ *
+ * @return the retry time interval.
+ * @since 0.17
+ */
+ @Nullable
+ public abstract Duration getRetryInterval();
+
+ /**
+ * Returns whether the {@link OcAgentTraceExporter} should handle the config streams.
+ *
+ * @return whether the {@code OcAgentTraceExporter} should handle the config streams.
+ * @since 0.17
+ */
+ public abstract boolean getEnableConfig();
+
+ /**
+ * Returns a new {@link Builder}.
+ *
+ * @return a {@code Builder}.
+ * @since 0.17
+ */
+ public static Builder builder() {
+ return new AutoValue_OcAgentTraceExporterConfiguration.Builder().setEnableConfig(true);
+ }
+
+ /**
+ * Builder for {@link OcAgentTraceExporterConfiguration}.
+ *
+ * @since 0.17
+ */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ Builder() {}
+
+ /**
+ * Sets the end point of OC-Agent server.
+ *
+ * @param endPoint the end point of OC-Agent.
+ * @return this.
+ * @since 0.17
+ */
+ public abstract Builder setEndPoint(String endPoint);
+
+ /**
+ * Sets whether to disable client transport security for the exporter's gRPC connection or not.
+ *
+ * @param useInsecure whether disable client transport security for the exporter's gRPC
+ * connection.
+ * @return this.
+ * @since 0.17
+ */
+ public abstract Builder setUseInsecure(Boolean useInsecure);
+
+ /**
+ * Sets the service name to be used for this {@link OcAgentTraceExporter}.
+ *
+ * @param serviceName the service name.
+ * @return this.
+ * @since 0.17
+ */
+ public abstract Builder setServiceName(String serviceName);
+
+ /**
+ * Sets the retry time interval when trying to connect to Agent.
+ *
+ * @param retryInterval the retry time interval.
+ * @return this.
+ * @since 0.17
+ */
+ public abstract Builder setRetryInterval(Duration retryInterval);
+
+ /**
+ * Sets whether {@link OcAgentTraceExporter} should handle the config streams.
+ *
+ * @param enableConfig whether {@code OcAgentTraceExporter} should handle the config streams.
+ * @return this.
+ * @since 0.17
+ */
+ public abstract Builder setEnableConfig(boolean enableConfig);
+
+ // TODO(songya): add an option that controls whether to always keep the RPC connection alive.
+
+ /**
+ * Builds a {@link OcAgentTraceExporterConfiguration}.
+ *
+ * @return a {@code OcAgentTraceExporterConfiguration}.
+ * @since 0.17
+ */
+ public abstract OcAgentTraceExporterConfiguration build();
+ }
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterHandler.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterHandler.java
new file mode 100644
index 00000000..5edc06df
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterHandler.java
@@ -0,0 +1,62 @@
+/*
+ * 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.ocagent;
+
+import io.opencensus.common.Duration;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import java.util.Collection;
+import javax.annotation.Nullable;
+
+/** Exporting handler for OC-Agent Tracing. */
+final class OcAgentTraceExporterHandler extends Handler {
+
+ private static final String DEFAULT_END_POINT = "localhost:55678";
+ private static final String DEFAULT_SERVICE_NAME = "OpenCensus";
+ private static final Duration DEFAULT_RETRY_INTERVAL = Duration.create(300, 0); // 5 minutes
+
+ OcAgentTraceExporterHandler() {
+ this(null, null, null, null, /* enableConfig= */ true);
+ }
+
+ OcAgentTraceExporterHandler(
+ @Nullable String endPoint,
+ @Nullable String serviceName,
+ @Nullable Boolean useInsecure,
+ @Nullable Duration retryInterval,
+ boolean enableConfig) {
+ // if (endPoint == null) {
+ // endPoint = DEFAULT_END_POINT;
+ // }
+ // if (serviceName == null) {
+ // serviceName = DEFAULT_SERVICE_NAME;
+ // }
+ // if (useInsecure == null) {
+ // useInsecure = false;
+ // }
+ // if (retryInterval == null) {
+ // retryInterval = DEFAULT_RETRY_INTERVAL;
+ // }
+ // OcAgentTraceServiceClients.startAttemptsToConnectToAgent(
+ // endPoint, useInsecure, serviceName, retryInterval.toMillis(), enableConfig);
+ }
+
+ @Override
+ public void export(Collection<SpanData> spanDataList) {
+ // OcAgentTraceServiceClients.onExport(spanDataList);
+ }
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtils.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtils.java
new file mode 100644
index 00000000..ec778ba6
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtils.java
@@ -0,0 +1,390 @@
+/*
+ * 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.ocagent;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.protobuf.BoolValue;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.UInt32Value;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.proto.agent.trace.v1.UpdatedLibraryConfig;
+import io.opencensus.proto.trace.v1.AttributeValue;
+import io.opencensus.proto.trace.v1.ConstantSampler;
+import io.opencensus.proto.trace.v1.ProbabilitySampler;
+import io.opencensus.proto.trace.v1.Span;
+import io.opencensus.proto.trace.v1.Span.Attributes;
+import io.opencensus.proto.trace.v1.Span.Link;
+import io.opencensus.proto.trace.v1.Span.Links;
+import io.opencensus.proto.trace.v1.Span.SpanKind;
+import io.opencensus.proto.trace.v1.Span.TimeEvent;
+import io.opencensus.proto.trace.v1.Span.TimeEvent.MessageEvent;
+import io.opencensus.proto.trace.v1.Span.Tracestate;
+import io.opencensus.proto.trace.v1.Span.Tracestate.Entry;
+import io.opencensus.proto.trace.v1.Status;
+import io.opencensus.proto.trace.v1.TraceConfig;
+import io.opencensus.proto.trace.v1.TruncatableString;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.MessageEvent.Type;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Utilities for converting the Tracing data models in OpenCensus Java to/from OpenCensus Proto. */
+final class TraceProtoUtils {
+
+ // Constant functions for AttributeValue.
+ private static final Function<String, /*@Nullable*/ AttributeValue> stringAttributeValueFunction =
+ new Function<String, /*@Nullable*/ AttributeValue>() {
+ @Override
+ public AttributeValue apply(String stringValue) {
+ return AttributeValue.newBuilder()
+ .setStringValue(toTruncatableStringProto(stringValue))
+ .build();
+ }
+ };
+
+ private static final Function<Boolean, /*@Nullable*/ AttributeValue>
+ booleanAttributeValueFunction =
+ new Function<Boolean, /*@Nullable*/ AttributeValue>() {
+ @Override
+ public AttributeValue apply(Boolean booleanValue) {
+ return AttributeValue.newBuilder().setBoolValue(booleanValue).build();
+ }
+ };
+
+ private static final Function<Long, /*@Nullable*/ AttributeValue> longAttributeValueFunction =
+ new Function<Long, /*@Nullable*/ AttributeValue>() {
+ @Override
+ public AttributeValue apply(Long longValue) {
+ return AttributeValue.newBuilder().setIntValue(longValue).build();
+ }
+ };
+
+ private static final Function<Double, /*@Nullable*/ AttributeValue> doubleAttributeValueFunction =
+ new Function<Double, /*@Nullable*/ AttributeValue>() {
+ @Override
+ public AttributeValue apply(Double doubleValue) {
+ return AttributeValue.newBuilder().setDoubleValue(doubleValue).build();
+ }
+ };
+
+ /**
+ * Converts {@link SpanData} to {@link Span} proto.
+ *
+ * @param spanData the {@code SpanData}.
+ * @return proto representation of {@code Span}.
+ */
+ static Span toSpanProto(SpanData spanData) {
+ SpanContext spanContext = spanData.getContext();
+ TraceId traceId = spanContext.getTraceId();
+ SpanId spanId = spanContext.getSpanId();
+ Span.Builder spanBuilder =
+ Span.newBuilder()
+ .setTraceId(toByteString(traceId.getBytes()))
+ .setSpanId(toByteString(spanId.getBytes()))
+ .setTracestate(toTracestateProto(spanContext.getTracestate()))
+ .setName(toTruncatableStringProto(spanData.getName()))
+ .setStartTime(toTimestampProto(spanData.getStartTimestamp()))
+ .setAttributes(toAttributesProto(spanData.getAttributes()))
+ .setTimeEvents(
+ toTimeEventsProto(spanData.getAnnotations(), spanData.getMessageEvents()))
+ .setLinks(toLinksProto(spanData.getLinks()));
+
+ Kind kind = spanData.getKind();
+ if (kind != null) {
+ spanBuilder.setKind(toSpanKindProto(kind));
+ }
+
+ io.opencensus.trace.Status status = spanData.getStatus();
+ if (status != null) {
+ spanBuilder.setStatus(toStatusProto(status));
+ }
+
+ Timestamp end = spanData.getEndTimestamp();
+ if (end != null) {
+ spanBuilder.setEndTime(toTimestampProto(end));
+ }
+
+ Integer childSpanCount = spanData.getChildSpanCount();
+ if (childSpanCount != null) {
+ spanBuilder.setChildSpanCount(UInt32Value.newBuilder().setValue(childSpanCount).build());
+ }
+
+ Boolean hasRemoteParent = spanData.getHasRemoteParent();
+ if (hasRemoteParent != null) {
+ spanBuilder.setSameProcessAsParentSpan(BoolValue.of(!hasRemoteParent));
+ }
+
+ SpanId parentSpanId = spanData.getParentSpanId();
+ if (parentSpanId != null && parentSpanId.isValid()) {
+ spanBuilder.setParentSpanId(toByteString(parentSpanId.getBytes()));
+ }
+
+ return spanBuilder.build();
+ }
+
+ @VisibleForTesting
+ static ByteString toByteString(byte[] bytes) {
+ return ByteString.copyFrom(bytes);
+ }
+
+ private static Tracestate toTracestateProto(io.opencensus.trace.Tracestate tracestate) {
+ return Tracestate.newBuilder().addAllEntries(toEntriesProto(tracestate.getEntries())).build();
+ }
+
+ private static List<Entry> toEntriesProto(List<io.opencensus.trace.Tracestate.Entry> entries) {
+ List<Entry> entriesProto = new ArrayList<Entry>();
+ for (io.opencensus.trace.Tracestate.Entry entry : entries) {
+ entriesProto.add(
+ Entry.newBuilder().setKey(entry.getKey()).setValue(entry.getValue()).build());
+ }
+ return entriesProto;
+ }
+
+ private static SpanKind toSpanKindProto(Kind kind) {
+ switch (kind) {
+ case CLIENT:
+ return SpanKind.CLIENT;
+ case SERVER:
+ return SpanKind.SERVER;
+ }
+ return SpanKind.UNRECOGNIZED;
+ }
+
+ private static Span.TimeEvents toTimeEventsProto(
+ TimedEvents<Annotation> annotationTimedEvents,
+ TimedEvents<io.opencensus.trace.MessageEvent> messageEventTimedEvents) {
+ Span.TimeEvents.Builder timeEventsBuilder = Span.TimeEvents.newBuilder();
+ timeEventsBuilder.setDroppedAnnotationsCount(annotationTimedEvents.getDroppedEventsCount());
+ for (TimedEvent<Annotation> annotation : annotationTimedEvents.getEvents()) {
+ timeEventsBuilder.addTimeEvent(toTimeAnnotationProto(annotation));
+ }
+ timeEventsBuilder.setDroppedMessageEventsCount(messageEventTimedEvents.getDroppedEventsCount());
+ for (TimedEvent<io.opencensus.trace.MessageEvent> networkEvent :
+ messageEventTimedEvents.getEvents()) {
+ timeEventsBuilder.addTimeEvent(toTimeMessageEventProto(networkEvent));
+ }
+ return timeEventsBuilder.build();
+ }
+
+ private static TimeEvent toTimeAnnotationProto(TimedEvent<Annotation> timedEvent) {
+ TimeEvent.Builder timeEventBuilder =
+ TimeEvent.newBuilder().setTime(toTimestampProto(timedEvent.getTimestamp()));
+ Annotation annotation = timedEvent.getEvent();
+ timeEventBuilder.setAnnotation(
+ TimeEvent.Annotation.newBuilder()
+ .setDescription(toTruncatableStringProto(annotation.getDescription()))
+ .setAttributes(toAttributesBuilderProto(annotation.getAttributes(), 0))
+ .build());
+ return timeEventBuilder.build();
+ }
+
+ private static TimeEvent toTimeMessageEventProto(
+ TimedEvent<io.opencensus.trace.MessageEvent> timedEvent) {
+ TimeEvent.Builder timeEventBuilder =
+ TimeEvent.newBuilder().setTime(toTimestampProto(timedEvent.getTimestamp()));
+ io.opencensus.trace.MessageEvent messageEvent = timedEvent.getEvent();
+ timeEventBuilder.setMessageEvent(
+ TimeEvent.MessageEvent.newBuilder()
+ .setId(messageEvent.getMessageId())
+ .setCompressedSize(messageEvent.getCompressedMessageSize())
+ .setUncompressedSize(messageEvent.getUncompressedMessageSize())
+ .setType(toMessageEventTypeProto(messageEvent))
+ .build());
+ return timeEventBuilder.build();
+ }
+
+ private static TimeEvent.MessageEvent.Type toMessageEventTypeProto(
+ io.opencensus.trace.MessageEvent messageEvent) {
+ if (messageEvent.getType() == Type.RECEIVED) {
+ return MessageEvent.Type.RECEIVED;
+ } else {
+ return MessageEvent.Type.SENT;
+ }
+ }
+
+ private static Attributes toAttributesProto(
+ io.opencensus.trace.export.SpanData.Attributes attributes) {
+ Attributes.Builder attributesBuilder =
+ toAttributesBuilderProto(
+ attributes.getAttributeMap(), attributes.getDroppedAttributesCount());
+ return attributesBuilder.build();
+ }
+
+ private static Attributes.Builder toAttributesBuilderProto(
+ Map<String, io.opencensus.trace.AttributeValue> attributes, int droppedAttributesCount) {
+ Attributes.Builder attributesBuilder =
+ Attributes.newBuilder().setDroppedAttributesCount(droppedAttributesCount);
+ for (Map.Entry<String, io.opencensus.trace.AttributeValue> label : attributes.entrySet()) {
+ AttributeValue value = toAttributeValueProto(label.getValue());
+ if (value != null) {
+ attributesBuilder.putAttributeMap(label.getKey(), value);
+ }
+ }
+ return attributesBuilder;
+ }
+
+ @javax.annotation.Nullable
+ private static AttributeValue toAttributeValueProto(
+ io.opencensus.trace.AttributeValue attributeValue) {
+ return attributeValue.match(
+ stringAttributeValueFunction,
+ booleanAttributeValueFunction,
+ longAttributeValueFunction,
+ doubleAttributeValueFunction,
+ Functions.</*@Nullable*/ AttributeValue>returnNull());
+ }
+
+ private static Status toStatusProto(io.opencensus.trace.Status status) {
+ Status.Builder statusBuilder = Status.newBuilder().setCode(status.getCanonicalCode().value());
+ if (status.getDescription() != null) {
+ statusBuilder.setMessage(status.getDescription());
+ }
+ return statusBuilder.build();
+ }
+
+ @VisibleForTesting
+ static TruncatableString toTruncatableStringProto(String string) {
+ return TruncatableString.newBuilder().setValue(string).setTruncatedByteCount(0).build();
+ }
+
+ static com.google.protobuf.Timestamp toTimestampProto(Timestamp timestamp) {
+ return com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(timestamp.getSeconds())
+ .setNanos(timestamp.getNanos())
+ .build();
+ }
+
+ private static Link.Type toLinkTypeProto(io.opencensus.trace.Link.Type type) {
+ if (type == io.opencensus.trace.Link.Type.PARENT_LINKED_SPAN) {
+ return Link.Type.PARENT_LINKED_SPAN;
+ } else {
+ return Link.Type.CHILD_LINKED_SPAN;
+ }
+ }
+
+ private static Link toLinkProto(io.opencensus.trace.Link link) {
+ return Link.newBuilder()
+ .setTraceId(toByteString(link.getTraceId().getBytes()))
+ .setSpanId(toByteString(link.getSpanId().getBytes()))
+ .setType(toLinkTypeProto(link.getType()))
+ .setAttributes(toAttributesBuilderProto(link.getAttributes(), 0))
+ .build();
+ }
+
+ private static Links toLinksProto(io.opencensus.trace.export.SpanData.Links links) {
+ final Links.Builder linksBuilder =
+ Links.newBuilder().setDroppedLinksCount(links.getDroppedLinksCount());
+ for (io.opencensus.trace.Link link : links.getLinks()) {
+ linksBuilder.addLink(toLinkProto(link));
+ }
+ return linksBuilder.build();
+ }
+
+ /**
+ * Converts {@link TraceParams} to {@link TraceConfig}.
+ *
+ * @param traceParams the {@code TraceParams}.
+ * @return {@code TraceConfig}.
+ */
+ static TraceConfig toTraceConfigProto(TraceParams traceParams) {
+ TraceConfig.Builder traceConfigProtoBuilder = TraceConfig.newBuilder();
+ Sampler librarySampler = traceParams.getSampler();
+
+ if (Samplers.alwaysSample().equals(librarySampler)) {
+ traceConfigProtoBuilder.setConstantSampler(
+ ConstantSampler.newBuilder().setDecision(true).build());
+ } else if (Samplers.neverSample().equals(librarySampler)) {
+ traceConfigProtoBuilder.setConstantSampler(
+ ConstantSampler.newBuilder().setDecision(false).build());
+ } else {
+ // TODO: consider exposing the sampling probability of ProbabilitySampler.
+ double samplingProbability = parseSamplingProbability(librarySampler);
+ traceConfigProtoBuilder.setProbabilitySampler(
+ ProbabilitySampler.newBuilder().setSamplingProbability(samplingProbability).build());
+ } // TODO: add support for RateLimitingSampler.
+
+ return traceConfigProtoBuilder.build();
+ }
+
+ private static double parseSamplingProbability(Sampler sampler) {
+ String description = sampler.getDescription();
+ // description follows format "ProbabilitySampler{%.6f}", samplingProbability.
+ int leftParenIndex = description.indexOf("{");
+ int rightParenIndex = description.indexOf("}");
+ return Double.parseDouble(description.substring(leftParenIndex + 1, rightParenIndex));
+ }
+
+ /**
+ * Converts {@link TraceConfig} to {@link TraceParams}.
+ *
+ * @param traceConfigProto {@code TraceConfig}.
+ * @param currentTraceParams current {@code TraceParams}.
+ * @return updated {@code TraceParams}.
+ * @since 0.17
+ */
+ static TraceParams fromTraceConfigProto(
+ TraceConfig traceConfigProto, TraceParams currentTraceParams) {
+ TraceParams.Builder builder = currentTraceParams.toBuilder();
+ if (traceConfigProto.hasConstantSampler()) {
+ ConstantSampler constantSampler = traceConfigProto.getConstantSampler();
+ if (Boolean.TRUE.equals(constantSampler.getDecision())) {
+ builder.setSampler(Samplers.alwaysSample());
+ } else {
+ builder.setSampler(Samplers.neverSample());
+ }
+ } else if (traceConfigProto.hasProbabilitySampler()) {
+ builder.setSampler(
+ Samplers.probabilitySampler(
+ traceConfigProto.getProbabilitySampler().getSamplingProbability()));
+ } // TODO: add support for RateLimitingSampler.
+ return builder.build();
+ }
+
+ // Creates a TraceConfig proto message with current TraceParams.
+ static TraceConfig getCurrentTraceConfig(io.opencensus.trace.config.TraceConfig traceConfig) {
+ TraceParams traceParams = traceConfig.getActiveTraceParams();
+ return toTraceConfigProto(traceParams);
+ }
+
+ // Creates an updated TraceParams with the given UpdatedLibraryConfig message and current
+ // TraceParams, then applies the updated TraceParams.
+ static TraceParams getUpdatedTraceParams(
+ UpdatedLibraryConfig config, io.opencensus.trace.config.TraceConfig traceConfig) {
+ TraceParams currentParams = traceConfig.getActiveTraceParams();
+ TraceConfig traceConfigProto = config.getConfig();
+ return fromTraceConfigProto(traceConfigProto, currentParams);
+ }
+
+ private TraceProtoUtils() {}
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/package-info.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/package-info.java
new file mode 100644
index 00000000..d01dd7eb
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+/**
+ * This package contains the Java implementation of the OpenCensus Agent (OC-Agent) Trace Exporter.
+ *
+ * <p>WARNING: Currently all the public classes under this package are marked as {@link
+ * io.opencensus.common.ExperimentalApi}. The classes and APIs under {@link
+ * io.opencensus.exporter.trace.ocagent} are likely to get backwards-incompatible updates in the
+ * future. DO NOT USE except for experimental purposes.
+ *
+ * <p>See more details on
+ * https://github.com/census-instrumentation/opencensus-proto/tree/master/src/opencensus/proto/agent.
+ */
+@io.opencensus.common.ExperimentalApi
+package io.opencensus.exporter.trace.ocagent;
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImpl.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImpl.java
new file mode 100644
index 00000000..fbdb35e3
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImpl.java
@@ -0,0 +1,169 @@
+/*
+ * 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.ocagent;
+
+import com.google.common.util.concurrent.MoreExecutors;
+import io.grpc.Server;
+import io.grpc.ServerBuilder;
+import io.grpc.netty.NettyServerBuilder;
+import io.grpc.stub.StreamObserver;
+import io.opencensus.proto.agent.trace.v1.CurrentLibraryConfig;
+import io.opencensus.proto.agent.trace.v1.ExportTraceServiceRequest;
+import io.opencensus.proto.agent.trace.v1.ExportTraceServiceResponse;
+import io.opencensus.proto.agent.trace.v1.TraceServiceGrpc;
+import io.opencensus.proto.agent.trace.v1.UpdatedLibraryConfig;
+import io.opencensus.proto.trace.v1.ConstantSampler;
+import io.opencensus.proto.trace.v1.TraceConfig;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/** Fake implementation of {@link TraceServiceGrpc}. */
+final class FakeOcAgentTraceServiceGrpcImpl extends TraceServiceGrpc.TraceServiceImplBase {
+
+ private static final Logger logger =
+ Logger.getLogger(FakeOcAgentTraceServiceGrpcImpl.class.getName());
+
+ // Default updatedLibraryConfig uses an always sampler.
+ private UpdatedLibraryConfig updatedLibraryConfig =
+ UpdatedLibraryConfig.newBuilder()
+ .setConfig(
+ TraceConfig.newBuilder()
+ .setConstantSampler(ConstantSampler.newBuilder().setDecision(true).build())
+ .build())
+ .build();
+
+ private final List<CurrentLibraryConfig> currentLibraryConfigs = new ArrayList<>();
+ private final List<ExportTraceServiceRequest> exportTraceServiceRequests = new ArrayList<>();
+
+ private final AtomicReference<StreamObserver<UpdatedLibraryConfig>> updatedConfigObserverRef =
+ new AtomicReference<>();
+
+ private final StreamObserver<CurrentLibraryConfig> currentConfigObserver =
+ new StreamObserver<CurrentLibraryConfig>() {
+ @Override
+ public void onNext(CurrentLibraryConfig value) {
+ currentLibraryConfigs.add(value);
+ @Nullable
+ StreamObserver<UpdatedLibraryConfig> updatedConfigObserver =
+ updatedConfigObserverRef.get();
+ if (updatedConfigObserver != null) {
+ updatedConfigObserver.onNext(updatedLibraryConfig);
+ }
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ logger.warning("Exception thrown for config stream: " + t);
+ }
+
+ @Override
+ public void onCompleted() {}
+ };
+
+ private final StreamObserver<ExportTraceServiceRequest> exportRequestObserver =
+ new StreamObserver<ExportTraceServiceRequest>() {
+ @Override
+ public void onNext(ExportTraceServiceRequest value) {
+ exportTraceServiceRequests.add(value);
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ logger.warning("Exception thrown for export stream: " + t);
+ }
+
+ @Override
+ public void onCompleted() {}
+ };
+
+ @Override
+ public StreamObserver<CurrentLibraryConfig> config(
+ StreamObserver<UpdatedLibraryConfig> updatedLibraryConfigStreamObserver) {
+ updatedConfigObserverRef.set(updatedLibraryConfigStreamObserver);
+ return currentConfigObserver;
+ }
+
+ @Override
+ public StreamObserver<ExportTraceServiceRequest> export(
+ StreamObserver<ExportTraceServiceResponse> exportTraceServiceResponseStreamObserver) {
+ return exportRequestObserver;
+ }
+
+ // Returns the stored CurrentLibraryConfigs.
+ List<CurrentLibraryConfig> getCurrentLibraryConfigs() {
+ return Collections.unmodifiableList(currentLibraryConfigs);
+ }
+
+ // Returns the stored ExportTraceServiceRequests.
+ List<ExportTraceServiceRequest> getExportTraceServiceRequests() {
+ return Collections.unmodifiableList(exportTraceServiceRequests);
+ }
+
+ // Sets the UpdatedLibraryConfig that will be passed to client.
+ void setUpdatedLibraryConfig(UpdatedLibraryConfig updatedLibraryConfig) {
+ this.updatedLibraryConfig = updatedLibraryConfig;
+ }
+
+ // Gets the UpdatedLibraryConfig that will be passed to client.
+ UpdatedLibraryConfig getUpdatedLibraryConfig() {
+ return updatedLibraryConfig;
+ }
+
+ static void startServer(String endPoint) throws IOException {
+ ServerBuilder<?> builder = NettyServerBuilder.forAddress(parseEndpoint(endPoint));
+ Executor executor = MoreExecutors.directExecutor();
+ builder.executor(executor);
+ final Server server = builder.addService(new FakeOcAgentTraceServiceGrpcImpl()).build();
+ server.start();
+ logger.info("Server started at " + endPoint);
+
+ Runtime.getRuntime()
+ .addShutdownHook(
+ new Thread() {
+ @Override
+ public void run() {
+ server.shutdown();
+ }
+ });
+
+ try {
+ server.awaitTermination();
+ } catch (InterruptedException e) {
+ logger.warning("Thread interrupted: " + e.getMessage());
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private static InetSocketAddress parseEndpoint(String endPoint) {
+ try {
+ int colonIndex = endPoint.indexOf(":");
+ String host = endPoint.substring(0, colonIndex);
+ int port = Integer.parseInt(endPoint.substring(colonIndex + 1));
+ return new InetSocketAddress(host, port);
+ } catch (RuntimeException e) {
+ logger.warning("Unexpected format of end point: " + endPoint + ", use default end point.");
+ return new InetSocketAddress("localhost", 55678);
+ }
+ }
+}
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImplTest.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImplTest.java
new file mode 100644
index 00000000..f619021b
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImplTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.ocagent;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.grpc.stub.StreamObserver;
+import io.opencensus.proto.agent.trace.v1.CurrentLibraryConfig;
+import io.opencensus.proto.agent.trace.v1.ExportTraceServiceRequest;
+import io.opencensus.proto.agent.trace.v1.ExportTraceServiceResponse;
+import io.opencensus.proto.agent.trace.v1.UpdatedLibraryConfig;
+import io.opencensus.proto.trace.v1.ConstantSampler;
+import io.opencensus.proto.trace.v1.TraceConfig;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link FakeOcAgentTraceServiceGrpcImpl}. */
+@RunWith(JUnit4.class)
+public class FakeOcAgentTraceServiceGrpcImplTest {
+
+ private final List<UpdatedLibraryConfig> updatedLibraryConfigs = new ArrayList<>();
+
+ private final StreamObserver<UpdatedLibraryConfig> updatedConfigObserver =
+ new StreamObserver<UpdatedLibraryConfig>() {
+
+ @Override
+ public void onNext(UpdatedLibraryConfig value) {
+ updatedLibraryConfigs.add(value);
+ }
+
+ @Override
+ public void onError(Throwable t) {}
+
+ @Override
+ public void onCompleted() {}
+ };
+
+ private final StreamObserver<ExportTraceServiceResponse> exportResponseObserver =
+ new StreamObserver<ExportTraceServiceResponse>() {
+ @Override
+ public void onNext(ExportTraceServiceResponse value) {}
+
+ @Override
+ public void onError(Throwable t) {}
+
+ @Override
+ public void onCompleted() {}
+ };
+
+ private static final UpdatedLibraryConfig neverSampledLibraryConfig =
+ UpdatedLibraryConfig.newBuilder()
+ .setConfig(
+ TraceConfig.newBuilder()
+ .setConstantSampler(ConstantSampler.newBuilder().setDecision(false).build())
+ .build())
+ .build();
+
+ @Test
+ public void export() {
+ FakeOcAgentTraceServiceGrpcImpl traceServiceGrpc = new FakeOcAgentTraceServiceGrpcImpl();
+ StreamObserver<ExportTraceServiceRequest> exportRequestObserver =
+ traceServiceGrpc.export(exportResponseObserver);
+ ExportTraceServiceRequest request = ExportTraceServiceRequest.getDefaultInstance();
+ exportRequestObserver.onNext(request);
+ assertThat(traceServiceGrpc.getExportTraceServiceRequests()).containsExactly(request);
+ }
+
+ @Test
+ public void config() {
+ FakeOcAgentTraceServiceGrpcImpl traceServiceGrpc = new FakeOcAgentTraceServiceGrpcImpl();
+ StreamObserver<CurrentLibraryConfig> currentConfigObsever =
+ traceServiceGrpc.config(updatedConfigObserver);
+ CurrentLibraryConfig currentLibraryConfig = CurrentLibraryConfig.getDefaultInstance();
+ currentConfigObsever.onNext(currentLibraryConfig);
+ assertThat(traceServiceGrpc.getCurrentLibraryConfigs()).containsExactly(currentLibraryConfig);
+ assertThat(updatedLibraryConfigs).containsExactly(traceServiceGrpc.getUpdatedLibraryConfig());
+ updatedLibraryConfigs.clear();
+ }
+
+ @Test
+ public void config_WithNeverSampler() {
+ FakeOcAgentTraceServiceGrpcImpl traceServiceGrpc = new FakeOcAgentTraceServiceGrpcImpl();
+ traceServiceGrpc.setUpdatedLibraryConfig(neverSampledLibraryConfig);
+ StreamObserver<CurrentLibraryConfig> currentConfigObsever =
+ traceServiceGrpc.config(updatedConfigObserver);
+ CurrentLibraryConfig currentLibraryConfig = CurrentLibraryConfig.getDefaultInstance();
+ currentConfigObsever.onNext(currentLibraryConfig);
+ assertThat(traceServiceGrpc.getCurrentLibraryConfigs()).containsExactly(currentLibraryConfig);
+ assertThat(updatedLibraryConfigs).containsExactly(neverSampledLibraryConfig);
+ updatedLibraryConfigs.clear();
+ }
+}
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtilsTest.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtilsTest.java
new file mode 100644
index 00000000..813066bc
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtilsTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.ocagent;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.trace.ocagent.OcAgentNodeUtils.OC_AGENT_EXPORTER_VERSION;
+import static io.opencensus.exporter.trace.ocagent.OcAgentNodeUtils.RESOURCE_LABEL_ATTRIBUTE_KEY;
+import static io.opencensus.exporter.trace.ocagent.OcAgentNodeUtils.RESOURCE_TYPE_ATTRIBUTE_KEY;
+
+import io.opencensus.common.Timestamp;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import io.opencensus.proto.agent.common.v1.LibraryInfo;
+import io.opencensus.proto.agent.common.v1.LibraryInfo.Language;
+import io.opencensus.proto.agent.common.v1.ProcessIdentifier;
+import io.opencensus.proto.agent.common.v1.ServiceInfo;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link OcAgentNodeUtils}. */
+@RunWith(JUnit4.class)
+public class OcAgentNodeUtilsTest {
+
+ private static final AwsEc2InstanceMonitoredResource AWS_RESOURCE =
+ AwsEc2InstanceMonitoredResource.create("account1", "instance1", "us-east-2");
+ private static final GcpGceInstanceMonitoredResource GCE_RESOURCE =
+ GcpGceInstanceMonitoredResource.create("account2", "instance2", "us-west2");
+ private static final GcpGkeContainerMonitoredResource GKE_RESOURCE =
+ GcpGkeContainerMonitoredResource.create(
+ "account3", "cluster", "container", "", "instance3", "", "us-west4");
+
+ @Test
+ public void testConstants() {
+ assertThat(OC_AGENT_EXPORTER_VERSION).isEqualTo("0.17.0-SNAPSHOT");
+ assertThat(RESOURCE_TYPE_ATTRIBUTE_KEY).isEqualTo("OPENCENSUS_SOURCE_TYPE");
+ assertThat(RESOURCE_LABEL_ATTRIBUTE_KEY).isEqualTo("OPENCENSUS_SOURCE_LABELS");
+ }
+
+ @Test
+ public void getProcessIdentifier() {
+ String jvmName = "54321@my.org";
+ Timestamp timestamp = Timestamp.create(10, 20);
+ ProcessIdentifier processIdentifier = OcAgentNodeUtils.getProcessIdentifier(jvmName, timestamp);
+ assertThat(processIdentifier.getHostName()).isEqualTo("my.org");
+ assertThat(processIdentifier.getPid()).isEqualTo(54321);
+ assertThat(processIdentifier.getStartTimestamp())
+ .isEqualTo(com.google.protobuf.Timestamp.newBuilder().setSeconds(10).setNanos(20).build());
+ }
+
+ @Test
+ public void getLibraryInfo() {
+ String currentOcJavaVersion = "0.16.0";
+ LibraryInfo libraryInfo = OcAgentNodeUtils.getLibraryInfo(currentOcJavaVersion);
+ assertThat(libraryInfo.getLanguage()).isEqualTo(Language.JAVA);
+ assertThat(libraryInfo.getCoreLibraryVersion()).isEqualTo(currentOcJavaVersion);
+ assertThat(libraryInfo.getExporterVersion()).isEqualTo(OC_AGENT_EXPORTER_VERSION);
+ }
+
+ @Test
+ public void getServiceInfo() {
+ String serviceName = "my-service";
+ ServiceInfo serviceInfo = OcAgentNodeUtils.getServiceInfo(serviceName);
+ assertThat(serviceInfo.getName()).isEqualTo(serviceName);
+ }
+
+ @Test
+ public void getAttributeMap_Null() {
+ Map<String, String> attributeMap = OcAgentNodeUtils.getAttributeMap(null);
+ assertThat(attributeMap).isEmpty();
+ }
+
+ @Test
+ public void getAttributeMap_AwsEc2Resource() {
+ Map<String, String> attributeMap = OcAgentNodeUtils.getAttributeMap(AWS_RESOURCE);
+ assertThat(attributeMap)
+ .containsExactly(
+ RESOURCE_TYPE_ATTRIBUTE_KEY,
+ "AWS_EC2_INSTANCE",
+ RESOURCE_LABEL_ATTRIBUTE_KEY,
+ "aws_account=account1,instance_id=instance1,region=us-east-2");
+ }
+
+ @Test
+ public void getAttributeMap_GceResource() {
+ Map<String, String> attributeMap = OcAgentNodeUtils.getAttributeMap(GCE_RESOURCE);
+ assertThat(attributeMap)
+ .containsExactly(
+ RESOURCE_TYPE_ATTRIBUTE_KEY,
+ "GCP_GCE_INSTANCE",
+ RESOURCE_LABEL_ATTRIBUTE_KEY,
+ "gcp_account=account2,instance_id=instance2,zone=us-west2");
+ }
+
+ @Test
+ public void getAttributeMap_GkeResource() {
+ Map<String, String> attributeMap = OcAgentNodeUtils.getAttributeMap(GKE_RESOURCE);
+ assertThat(attributeMap)
+ .containsExactly(
+ RESOURCE_TYPE_ATTRIBUTE_KEY,
+ "GCP_GKE_CONTAINER",
+ RESOURCE_LABEL_ATTRIBUTE_KEY,
+ "gcp_account=account3,instance_id=instance3,location=us-west4,"
+ + "cluster_name=cluster,container_name=container");
+ }
+}
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfigurationTest.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfigurationTest.java
new file mode 100644
index 00000000..81bc5c60
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfigurationTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.ocagent;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link OcAgentTraceExporterConfiguration}. */
+@RunWith(JUnit4.class)
+public class OcAgentTraceExporterConfigurationTest {
+
+ @Test
+ public void defaultConfiguration() {
+ OcAgentTraceExporterConfiguration configuration =
+ OcAgentTraceExporterConfiguration.builder().build();
+ assertThat(configuration.getEndPoint()).isNull();
+ assertThat(configuration.getServiceName()).isNull();
+ assertThat(configuration.getUseInsecure()).isNull();
+ assertThat(configuration.getRetryInterval()).isNull();
+ assertThat(configuration.getEnableConfig()).isTrue();
+ }
+
+ @Test
+ public void setAndGet() {
+ Duration oneMinute = Duration.create(60, 0);
+ OcAgentTraceExporterConfiguration configuration =
+ OcAgentTraceExporterConfiguration.builder()
+ .setEndPoint("192.168.0.1:50051")
+ .setServiceName("service")
+ .setUseInsecure(true)
+ .setRetryInterval(oneMinute)
+ .setEnableConfig(false)
+ .build();
+ assertThat(configuration.getEndPoint()).isEqualTo("192.168.0.1:50051");
+ assertThat(configuration.getServiceName()).isEqualTo("service");
+ assertThat(configuration.getUseInsecure()).isTrue();
+ assertThat(configuration.getRetryInterval()).isEqualTo(oneMinute);
+ assertThat(configuration.getEnableConfig()).isFalse();
+ }
+}
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterTest.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterTest.java
new file mode 100644
index 00000000..c58acdb1
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.ocagent;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+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;
+
+/** Unit tests for {@link OcAgentTraceExporter}. */
+@RunWith(JUnit4.class)
+public class OcAgentTraceExporterTest {
+ @Mock private SpanExporter spanExporter;
+ @Mock private Handler handler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void registerUnregisterOcAgentTraceExporter() {
+ OcAgentTraceExporter.register(spanExporter, handler);
+ verify(spanExporter)
+ .registerHandler(
+ eq("io.opencensus.exporter.trace.ocagent.OcAgentTraceExporter"),
+ any(OcAgentTraceExporterHandler.class));
+ OcAgentTraceExporter.unregister(spanExporter);
+ verify(spanExporter)
+ .unregisterHandler(eq("io.opencensus.exporter.trace.ocagent.OcAgentTraceExporter"));
+ }
+}
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtilsTest.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtilsTest.java
new file mode 100644
index 00000000..74c7c29e
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtilsTest.java
@@ -0,0 +1,357 @@
+/*
+ * 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.ocagent;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.trace.ocagent.TraceProtoUtils.toByteString;
+import static io.opencensus.exporter.trace.ocagent.TraceProtoUtils.toTruncatableStringProto;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.protobuf.BoolValue;
+import com.google.protobuf.UInt32Value;
+import io.opencensus.common.Timestamp;
+import io.opencensus.proto.agent.trace.v1.UpdatedLibraryConfig;
+import io.opencensus.proto.trace.v1.AttributeValue;
+import io.opencensus.proto.trace.v1.ConstantSampler;
+import io.opencensus.proto.trace.v1.ProbabilitySampler;
+import io.opencensus.proto.trace.v1.Span;
+import io.opencensus.proto.trace.v1.Span.SpanKind;
+import io.opencensus.proto.trace.v1.Span.TimeEvent;
+import io.opencensus.proto.trace.v1.Span.TimeEvent.MessageEvent;
+import io.opencensus.proto.trace.v1.TraceConfig;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span.Kind;
+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.Tracestate;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.List;
+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.Mockito;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for {@link TraceProtoUtils}. */
+@RunWith(JUnit4.class)
+public class TraceProtoUtilsTest {
+
+ @Mock private io.opencensus.trace.config.TraceConfig mockTraceConfig;
+
+ private static final TraceParams DEFAULT_PARAMS = TraceParams.DEFAULT;
+
+ private static final Timestamp startTimestamp = Timestamp.create(123, 456);
+ private static final Timestamp eventTimestamp1 = Timestamp.create(123, 457);
+ private static final Timestamp eventTimestamp2 = Timestamp.create(123, 458);
+ private static final Timestamp eventTimestamp3 = Timestamp.create(123, 459);
+ private static final Timestamp endTimestamp = Timestamp.create(123, 460);
+
+ private static final String TRACE_ID = "4bf92f3577b34da6a3ce929d0e0e4736";
+ private static final String SPAN_ID = "24aa0b2d371f48c9";
+ private static final String PARENT_SPAN_ID = "71da8d631536f5f1";
+ private static final String SPAN_NAME = "MySpanName";
+ private static final String ANNOTATION_TEXT = "MyAnnotationText";
+ private static final String ATTRIBUTE_KEY_1 = "MyAttributeKey1";
+ private static final String ATTRIBUTE_KEY_2 = "MyAttributeKey2";
+
+ private static final String FIRST_KEY = "key_1";
+ private static final String SECOND_KEY = "key_2";
+ private static final String FIRST_VALUE = "value.1";
+ private static final String SECOND_VALUE = "value.2";
+ private static final Tracestate multiValueTracestate =
+ Tracestate.builder().set(FIRST_KEY, FIRST_VALUE).set(SECOND_KEY, SECOND_VALUE).build();
+
+ private static final int DROPPED_ATTRIBUTES_COUNT = 1;
+ private static final int DROPPED_ANNOTATIONS_COUNT = 2;
+ private static final int DROPPED_NETWORKEVENTS_COUNT = 3;
+ private static final int DROPPED_LINKS_COUNT = 4;
+ private static final int CHILD_SPAN_COUNT = 13;
+
+ private static final Annotation annotation = Annotation.fromDescription(ANNOTATION_TEXT);
+ private static final io.opencensus.trace.MessageEvent recvMessageEvent =
+ io.opencensus.trace.MessageEvent.builder(io.opencensus.trace.MessageEvent.Type.RECEIVED, 1)
+ .build();
+ private static final io.opencensus.trace.MessageEvent sentMessageEvent =
+ io.opencensus.trace.MessageEvent.builder(io.opencensus.trace.MessageEvent.Type.SENT, 1)
+ .build();
+ private static final Status status = Status.DEADLINE_EXCEEDED.withDescription("TooSlow");
+ private static final SpanId parentSpanId = SpanId.fromLowerBase16(PARENT_SPAN_ID);
+ private static final SpanId spanId = SpanId.fromLowerBase16(SPAN_ID);
+ private static final TraceId traceId = TraceId.fromLowerBase16(TRACE_ID);
+ private static final TraceOptions traceOptions = TraceOptions.DEFAULT;
+ private static final SpanContext spanContext =
+ SpanContext.create(traceId, spanId, traceOptions, multiValueTracestate);
+
+ private static final List<TimedEvent<Annotation>> annotationsList =
+ ImmutableList.of(
+ SpanData.TimedEvent.create(eventTimestamp1, annotation),
+ SpanData.TimedEvent.create(eventTimestamp3, annotation));
+ private static final List<TimedEvent<io.opencensus.trace.MessageEvent>> networkEventsList =
+ ImmutableList.of(
+ SpanData.TimedEvent.create(eventTimestamp1, recvMessageEvent),
+ SpanData.TimedEvent.create(eventTimestamp2, sentMessageEvent));
+ private static final List<Link> linksList =
+ ImmutableList.of(Link.fromSpanContext(spanContext, Link.Type.CHILD_LINKED_SPAN));
+
+ private static final SpanData.Attributes attributes =
+ SpanData.Attributes.create(
+ ImmutableMap.of(
+ ATTRIBUTE_KEY_1,
+ io.opencensus.trace.AttributeValue.longAttributeValue(10L),
+ ATTRIBUTE_KEY_2,
+ io.opencensus.trace.AttributeValue.booleanAttributeValue(true)),
+ DROPPED_ATTRIBUTES_COUNT);
+ private static final TimedEvents<Annotation> annotations =
+ TimedEvents.create(annotationsList, DROPPED_ANNOTATIONS_COUNT);
+ private static final TimedEvents<io.opencensus.trace.MessageEvent> messageEvents =
+ TimedEvents.create(networkEventsList, DROPPED_NETWORKEVENTS_COUNT);
+ private static final SpanData.Links links = SpanData.Links.create(linksList, DROPPED_LINKS_COUNT);
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ Mockito.when(mockTraceConfig.getActiveTraceParams()).thenReturn(DEFAULT_PARAMS);
+ Mockito.doNothing()
+ .when(mockTraceConfig)
+ .updateActiveTraceParams(Mockito.any(TraceParams.class));
+ }
+
+ @Test
+ public void toSpanProto() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ /* hasRemoteParent= */ false,
+ SPAN_NAME,
+ Kind.CLIENT,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ TimeEvent annotationTimeEvent1 =
+ TimeEvent.newBuilder()
+ .setAnnotation(
+ TimeEvent.Annotation.newBuilder()
+ .setDescription(toTruncatableStringProto(ANNOTATION_TEXT))
+ .setAttributes(Span.Attributes.newBuilder().build())
+ .build())
+ .setTime(
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(eventTimestamp1.getSeconds())
+ .setNanos(eventTimestamp1.getNanos())
+ .build())
+ .build();
+ TimeEvent annotationTimeEvent2 =
+ TimeEvent.newBuilder()
+ .setAnnotation(
+ TimeEvent.Annotation.newBuilder()
+ .setDescription(toTruncatableStringProto(ANNOTATION_TEXT))
+ .setAttributes(Span.Attributes.newBuilder().build())
+ .build())
+ .setTime(
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(eventTimestamp3.getSeconds())
+ .setNanos(eventTimestamp3.getNanos())
+ .build())
+ .build();
+
+ TimeEvent sentTimeEvent =
+ TimeEvent.newBuilder()
+ .setMessageEvent(
+ TimeEvent.MessageEvent.newBuilder()
+ .setType(MessageEvent.Type.SENT)
+ .setId(sentMessageEvent.getMessageId()))
+ .setTime(
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(eventTimestamp2.getSeconds())
+ .setNanos(eventTimestamp2.getNanos())
+ .build())
+ .build();
+ TimeEvent recvTimeEvent =
+ TimeEvent.newBuilder()
+ .setMessageEvent(
+ TimeEvent.MessageEvent.newBuilder()
+ .setType(MessageEvent.Type.RECEIVED)
+ .setId(recvMessageEvent.getMessageId()))
+ .setTime(
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(eventTimestamp1.getSeconds())
+ .setNanos(eventTimestamp1.getNanos())
+ .build())
+ .build();
+
+ Span.Links spanLinks =
+ Span.Links.newBuilder()
+ .setDroppedLinksCount(DROPPED_LINKS_COUNT)
+ .addLink(
+ Span.Link.newBuilder()
+ .setType(Span.Link.Type.CHILD_LINKED_SPAN)
+ .setTraceId(toByteString(traceId.getBytes()))
+ .setSpanId(toByteString(spanId.getBytes()))
+ .setAttributes(Span.Attributes.newBuilder().build())
+ .build())
+ .build();
+
+ io.opencensus.proto.trace.v1.Status spanStatus =
+ io.opencensus.proto.trace.v1.Status.newBuilder()
+ .setCode(com.google.rpc.Code.DEADLINE_EXCEEDED.getNumber())
+ .setMessage("TooSlow")
+ .build();
+
+ com.google.protobuf.Timestamp startTime =
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(startTimestamp.getSeconds())
+ .setNanos(startTimestamp.getNanos())
+ .build();
+ com.google.protobuf.Timestamp endTime =
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(endTimestamp.getSeconds())
+ .setNanos(endTimestamp.getNanos())
+ .build();
+
+ Span span = TraceProtoUtils.toSpanProto(spanData);
+ assertThat(span.getName()).isEqualTo(toTruncatableStringProto(SPAN_NAME));
+ assertThat(span.getTraceId()).isEqualTo(toByteString(traceId.getBytes()));
+ assertThat(span.getSpanId()).isEqualTo(toByteString(spanId.getBytes()));
+ assertThat(span.getParentSpanId()).isEqualTo(toByteString(parentSpanId.getBytes()));
+ assertThat(span.getStartTime()).isEqualTo(startTime);
+ assertThat(span.getEndTime()).isEqualTo(endTime);
+ assertThat(span.getKind()).isEqualTo(SpanKind.CLIENT);
+ assertThat(span.getAttributes().getDroppedAttributesCount())
+ .isEqualTo(DROPPED_ATTRIBUTES_COUNT);
+ // The generated attributes map contains more values (e.g. agent). We only test what we added.
+ assertThat(span.getAttributes().getAttributeMapMap())
+ .containsEntry(ATTRIBUTE_KEY_1, AttributeValue.newBuilder().setIntValue(10L).build());
+ assertThat(span.getAttributes().getAttributeMapMap())
+ .containsEntry(ATTRIBUTE_KEY_2, AttributeValue.newBuilder().setBoolValue(true).build());
+ assertThat(span.getTimeEvents().getDroppedMessageEventsCount())
+ .isEqualTo(DROPPED_NETWORKEVENTS_COUNT);
+ assertThat(span.getTimeEvents().getDroppedAnnotationsCount())
+ .isEqualTo(DROPPED_ANNOTATIONS_COUNT);
+ assertThat(span.getTimeEvents().getTimeEventList())
+ .containsAllOf(annotationTimeEvent1, annotationTimeEvent2, sentTimeEvent, recvTimeEvent);
+ assertThat(span.getLinks()).isEqualTo(spanLinks);
+ assertThat(span.getStatus()).isEqualTo(spanStatus);
+ assertThat(span.getSameProcessAsParentSpan()).isEqualTo(BoolValue.of(true));
+ assertThat(span.getChildSpanCount())
+ .isEqualTo(UInt32Value.newBuilder().setValue(CHILD_SPAN_COUNT).build());
+ }
+
+ @Test
+ public void toTraceConfigProto_AlwaysSampler() {
+ assertThat(TraceProtoUtils.toTraceConfigProto(getTraceParams(Samplers.alwaysSample())))
+ .isEqualTo(
+ TraceConfig.newBuilder()
+ .setConstantSampler(ConstantSampler.newBuilder().setDecision(true).build())
+ .build());
+ }
+
+ @Test
+ public void toTraceConfigProto_NeverSampler() {
+ assertThat(TraceProtoUtils.toTraceConfigProto(getTraceParams(Samplers.neverSample())))
+ .isEqualTo(
+ TraceConfig.newBuilder()
+ .setConstantSampler(ConstantSampler.newBuilder().setDecision(false).build())
+ .build());
+ }
+
+ @Test
+ public void toTraceConfigProto_ProbabilitySampler() {
+ assertThat(TraceProtoUtils.toTraceConfigProto(getTraceParams(Samplers.probabilitySampler(0.5))))
+ .isEqualTo(
+ TraceConfig.newBuilder()
+ .setProbabilitySampler(
+ ProbabilitySampler.newBuilder().setSamplingProbability(0.5).build())
+ .build());
+ }
+
+ @Test
+ public void fromTraceConfigProto_AlwaysSampler() {
+ TraceConfig traceConfig =
+ TraceConfig.newBuilder()
+ .setConstantSampler(ConstantSampler.newBuilder().setDecision(true).build())
+ .build();
+ assertThat(TraceProtoUtils.fromTraceConfigProto(traceConfig, DEFAULT_PARAMS).getSampler())
+ .isEqualTo(Samplers.alwaysSample());
+ }
+
+ @Test
+ public void fromTraceConfigProto_NeverSampler() {
+ TraceConfig traceConfig =
+ TraceConfig.newBuilder()
+ .setConstantSampler(ConstantSampler.newBuilder().setDecision(false).build())
+ .build();
+ assertThat(TraceProtoUtils.fromTraceConfigProto(traceConfig, DEFAULT_PARAMS).getSampler())
+ .isEqualTo(Samplers.neverSample());
+ }
+
+ @Test
+ public void fromTraceConfigProto_ProbabilitySampler() {
+ TraceConfig traceConfig =
+ TraceConfig.newBuilder()
+ .setProbabilitySampler(
+ ProbabilitySampler.newBuilder().setSamplingProbability(0.01).build())
+ .build();
+ assertThat(TraceProtoUtils.fromTraceConfigProto(traceConfig, DEFAULT_PARAMS).getSampler())
+ .isEqualTo(Samplers.probabilitySampler(0.01));
+ }
+
+ @Test
+ public void getCurrentTraceConfig() {
+ TraceConfig configProto = TraceProtoUtils.toTraceConfigProto(DEFAULT_PARAMS);
+ assertThat(TraceProtoUtils.getCurrentTraceConfig(mockTraceConfig)).isEqualTo(configProto);
+ Mockito.verify(mockTraceConfig, Mockito.times(1)).getActiveTraceParams();
+ }
+
+ @Test
+ public void applyUpdatedConfig() {
+ TraceConfig configProto =
+ TraceConfig.newBuilder()
+ .setProbabilitySampler(
+ ProbabilitySampler.newBuilder().setSamplingProbability(0.01).build())
+ .build();
+ UpdatedLibraryConfig updatedLibraryConfig =
+ UpdatedLibraryConfig.newBuilder().setConfig(configProto).build();
+ TraceParams traceParams =
+ TraceProtoUtils.getUpdatedTraceParams(updatedLibraryConfig, mockTraceConfig);
+ TraceParams expectedParams =
+ DEFAULT_PARAMS.toBuilder().setSampler(Samplers.probabilitySampler(0.01)).build();
+ Mockito.verify(mockTraceConfig, Mockito.times(1)).getActiveTraceParams();
+ assertThat(traceParams).isEqualTo(expectedParams);
+ }
+
+ private static TraceParams getTraceParams(Sampler sampler) {
+ return DEFAULT_PARAMS.toBuilder().setSampler(sampler).build();
+ }
+}
diff --git a/exporters/trace/stackdriver/README.md b/exporters/trace/stackdriver/README.md
new file mode 100644
index 00000000..9186a47c
--- /dev/null
+++ b/exporters/trace/stackdriver/README.md
@@ -0,0 +1,127 @@
+# OpenCensus Stackdriver Trace Exporter
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Stackdriver Trace Exporter* is a trace exporter that exports data to
+Stackdriver Trace. [Stackdriver Trace][stackdriver-trace] is a distributed
+tracing system that collects latency data from your applications and displays it in the Google
+Cloud Platform Console. You can track how requests propagate through your application and receive
+detailed near real-time performance insights.
+
+## Quickstart
+
+### Prerequisites
+
+To use this exporter, you must have an application that you'd like to trace. The app can be on
+Google Cloud Platform, on-premise, or another cloud platform.
+
+In order to be able to push your traces to [Stackdriver Trace][stackdriver-trace], you must:
+
+1. [Create a Cloud project](https://support.google.com/cloud/answer/6251787?hl=en).
+2. [Enable billing](https://support.google.com/cloud/answer/6288653#new-billing).
+3. [Enable the Stackdriver Trace API](https://console.cloud.google.com/apis/api/cloudtrace.googleapis.com/overview).
+
+These steps enable the API but don't require that your app is hosted on Google Cloud Platform.
+
+### Hello "Stackdriver Trace"
+
+#### 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.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-trace-stackdriver</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-trace-stackdriver:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### Register the exporter
+
+This uses the default configuration for authentication and project ID.
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) throws Exception {
+ StackdriverTraceExporter.createAndRegister(
+ StackdriverTraceConfiguration.builder().build());
+ // ...
+ }
+}
+```
+
+#### Authentication
+
+This exporter uses [google-cloud-java](https://github.com/GoogleCloudPlatform/google-cloud-java),
+for details about how to configure the authentication see [here](https://github.com/GoogleCloudPlatform/google-cloud-java#authentication).
+
+If you prefer to manually set the credentials use:
+```
+StackdriverTraceExporter.createAndRegisterWithCredentialsAndProjectId(
+ new GoogleCredentials(new AccessToken(accessToken, expirationTime)),
+ "MyStackdriverProjectId");
+```
+
+#### Specifying a Project ID
+
+This exporter uses [google-cloud-java](https://github.com/GoogleCloudPlatform/google-cloud-java),
+for details about how to configure the project ID see [here](https://github.com/GoogleCloudPlatform/google-cloud-java#specifying-a-project-id).
+
+If you prefer to manually set the project ID use:
+```
+StackdriverTraceExporter.createAndRegisterWithProjectId("MyStackdriverProjectId");
+```
+
+#### Enable Stackdriver Trace API access scope on Google Cloud Platform
+If your Stackdriver Trace Exporter is running on Kubernetes Engine or Compute Engine,
+you might need additional setup to explicitly enable the ```trace.append``` Stackdriver
+Trace API access scope. To do that, please follow the instructions for
+[GKE](https://cloud.google.com/trace/docs/setup/java#kubernetes_engine) or
+[GCE](https://cloud.google.com/trace/docs/setup/java#compute_engine).
+
+#### Java Versions
+
+Java 7 or above is required for using this exporter.
+
+## FAQ
+### Why do I not see some trace events in Stackdriver?
+In all the versions before '0.9.1' the Stackdriver Trace exporter was implemented using the [v1
+API][stackdriver-v1-api-url] which is not fully compatible with the OpenCensus data model. Trace
+events like Annotations and NetworkEvents will be dropped.
+
+### Why do I get a "StatusRuntimeException: NOT_FOUND: Requested entity was not found"?
+One of the possible reasons is you are using a project id with bad format for the exporter.
+Please double check the project id associated with the Stackdriver Trace exporter first.
+Stackdriver Trace backend will not do any sanitization or trimming on the incoming project id.
+Project id with leading or trailing spaces will be treated as a separate non-existing project
+(e.g "project-id" vs "project-id "), and will cause a NOT_FOUND exception.
+
+[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-stackdriver/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-stackdriver
+[stackdriver-trace]: https://cloud.google.com/trace/
+[stackdriver-v1-api-url]: https://cloud.google.com/trace/docs/reference/v1/rpc/google.devtools.cloudtrace.v1#google.devtools.cloudtrace.v1.TraceSpan
diff --git a/exporters/trace/stackdriver/build.gradle b/exporters/trace/stackdriver/build.gradle
new file mode 100644
index 00000000..83dc970e
--- /dev/null
+++ b/exporters/trace/stackdriver/build.gradle
@@ -0,0 +1,31 @@
+description = 'OpenCensus Trace Stackdriver Exporter'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.7
+ it.targetCompatibility = 1.7
+}
+
+dependencies {
+ compileOnly libraries.auto_value
+
+ compile project(':opencensus-api'),
+ project(':opencensus-contrib-monitored-resource-util'),
+ libraries.google_auth,
+ libraries.guava
+
+ compile (libraries.google_cloud_trace) {
+ // Prefer library version.
+ exclude group: 'com.google.guava', module: 'guava'
+
+ // Prefer library version.
+ exclude group: 'com.google.code.findbugs', module: 'jsr305'
+
+ // We will always be more up to date.
+ exclude group: 'io.opencensus', module: 'opencensus-api'
+ }
+
+ testCompile project(':opencensus-api')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverExporter.java b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverExporter.java
new file mode 100644
index 00000000..8797cc77
--- /dev/null
+++ b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverExporter.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2017, 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.stackdriver;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.cloud.ServiceOptions;
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import java.io.IOException;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Stackdriver Trace.
+ *
+ * <p>Example of usage on Google Cloud VMs:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * StackdriverExporter.createAndRegisterWithProjectId("MyStackdriverProjectId");
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @deprecated Deprecated due to inconsistent naming. Use {@link StackdriverTraceExporter}.
+ * @since 0.6
+ */
+@Deprecated
+public final class StackdriverExporter {
+
+ /**
+ * Creates and registers the Stackdriver Trace exporter to the OpenCensus library for an explicit
+ * project ID and using explicit credentials. Only one Stackdriver exporter can be registered at
+ * any point.
+ *
+ * @param credentials a credentials used to authenticate API calls.
+ * @param projectId the cloud project id.
+ * @throws IllegalStateException if a Stackdriver exporter is already registered.
+ * @since 0.6
+ */
+ public static void createAndRegisterWithCredentialsAndProjectId(
+ Credentials credentials, String projectId) throws IOException {
+ StackdriverTraceExporter.createAndRegister(
+ StackdriverTraceConfiguration.builder()
+ .setCredentials(credentials)
+ .setProjectId(projectId)
+ .build());
+ }
+
+ /**
+ * Creates and registers the Stackdriver Trace exporter to the OpenCensus library for an explicit
+ * project ID. Only one Stackdriver exporter can be registered at any point.
+ *
+ * <p>This uses the default application credentials see {@link
+ * GoogleCredentials#getApplicationDefault}.
+ *
+ * <p>This is equivalent with:
+ *
+ * <pre>{@code
+ * StackdriverExporter.createAndRegisterWithCredentialsAndProjectId(
+ * GoogleCredentials.getApplicationDefault(), projectId);
+ * }</pre>
+ *
+ * @param projectId the cloud project id.
+ * @throws IllegalStateException if a Stackdriver exporter is already registered.
+ * @since 0.6
+ */
+ public static void createAndRegisterWithProjectId(String projectId) throws IOException {
+ StackdriverTraceExporter.createAndRegister(
+ StackdriverTraceConfiguration.builder()
+ .setCredentials(GoogleCredentials.getApplicationDefault())
+ .setProjectId(projectId)
+ .build());
+ }
+
+ /**
+ * Creates and registers the Stackdriver Trace exporter to the OpenCensus library. Only one
+ * Stackdriver exporter can be registered at any point.
+ *
+ * <p>This uses the default application credentials see {@link
+ * GoogleCredentials#getApplicationDefault}.
+ *
+ * <p>This uses the default project ID configured see {@link ServiceOptions#getDefaultProjectId}.
+ *
+ * <p>This is equivalent with:
+ *
+ * <pre>{@code
+ * StackdriverExporter.createAndRegisterWithProjectId(ServiceOptions.getDefaultProjectId());
+ * }</pre>
+ *
+ * @throws IllegalStateException if a Stackdriver exporter is already registered.
+ * @since 0.6
+ */
+ public static void createAndRegister() throws IOException {
+ StackdriverTraceExporter.createAndRegister(
+ StackdriverTraceConfiguration.builder()
+ .setCredentials(GoogleCredentials.getApplicationDefault())
+ .setProjectId(ServiceOptions.getDefaultProjectId())
+ .build());
+ }
+
+ /**
+ * Registers the {@code StackdriverExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(SpanExporter spanExporter, Handler handler) {
+ StackdriverTraceExporter.register(spanExporter, handler);
+ }
+
+ /**
+ * Unregisters the Stackdriver Trace exporter from the OpenCensus library.
+ *
+ * @throws IllegalStateException if a Stackdriver exporter is not registered.
+ * @since 0.6
+ */
+ public static void unregister() {
+ StackdriverTraceExporter.unregister();
+ }
+
+ /**
+ * Unregisters the {@code StackdriverExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(SpanExporter spanExporter) {
+ StackdriverTraceExporter.unregister(spanExporter);
+ }
+
+ private StackdriverExporter() {}
+}
diff --git a/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfiguration.java b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfiguration.java
new file mode 100644
index 00000000..f78832d0
--- /dev/null
+++ b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfiguration.java
@@ -0,0 +1,118 @@
+/*
+ * 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.stackdriver;
+
+import com.google.auth.Credentials;
+import com.google.auto.value.AutoValue;
+import com.google.cloud.trace.v2.stub.TraceServiceStub;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Configurations for {@link StackdriverTraceExporter}.
+ *
+ * @since 0.12
+ */
+@AutoValue
+@Immutable
+public abstract class StackdriverTraceConfiguration {
+
+ StackdriverTraceConfiguration() {}
+
+ /**
+ * Returns the {@link Credentials}.
+ *
+ * @return the {@code Credentials}.
+ * @since 0.12
+ */
+ @Nullable
+ public abstract Credentials getCredentials();
+
+ /**
+ * Returns the cloud project id.
+ *
+ * @return the cloud project id.
+ * @since 0.12
+ */
+ @Nullable
+ public abstract String getProjectId();
+
+ /**
+ * Returns a TraceServiceStub instance used to make RPC calls.
+ *
+ * @return the trace service stub.
+ * @since 0.16
+ */
+ @Nullable
+ public abstract TraceServiceStub getTraceServiceStub();
+
+ /**
+ * Returns a new {@link Builder}.
+ *
+ * @return a {@code Builder}.
+ * @since 0.12
+ */
+ public static Builder builder() {
+ return new AutoValue_StackdriverTraceConfiguration.Builder();
+ }
+
+ /**
+ * Builder for {@link StackdriverTraceConfiguration}.
+ *
+ * @since 0.12
+ */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ Builder() {}
+
+ /**
+ * Sets the {@link Credentials} used to authenticate API calls.
+ *
+ * @param credentials the {@code Credentials}.
+ * @return this.
+ * @since 0.12
+ */
+ public abstract Builder setCredentials(Credentials credentials);
+
+ /**
+ * Sets the cloud project id.
+ *
+ * @param projectId the cloud project id.
+ * @return this.
+ * @since 0.12
+ */
+ public abstract Builder setProjectId(String projectId);
+
+ /**
+ * Sets the trace service stub used to send gRPC calls.
+ *
+ * @param traceServiceStub the {@code TraceServiceStub}.
+ * @return this.
+ * @since 0.16
+ */
+ public abstract Builder setTraceServiceStub(TraceServiceStub traceServiceStub);
+
+ /**
+ * Builds a {@link StackdriverTraceConfiguration}.
+ *
+ * @return a {@code StackdriverTraceConfiguration}.
+ * @since 0.12
+ */
+ public abstract StackdriverTraceConfiguration build();
+ }
+}
diff --git a/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporter.java b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporter.java
new file mode 100644
index 00000000..0182ae94
--- /dev/null
+++ b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporter.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2017, 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.stackdriver;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.cloud.ServiceOptions;
+import com.google.cloud.trace.v2.TraceServiceClient;
+import com.google.cloud.trace.v2.stub.TraceServiceStub;
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import java.io.IOException;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Stackdriver Trace.
+ *
+ * <p>Example of usage on Google Cloud VMs:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * StackdriverTraceExporter.createAndRegister(
+ * StackdriverTraceConfiguration.builder()
+ * .setProjectId("MyStackdriverProjectId")
+ * .build());
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @since 0.12
+ */
+public final class StackdriverTraceExporter {
+
+ private static final String REGISTER_NAME = StackdriverTraceExporter.class.getName();
+ private static final Object monitor = new Object();
+
+ @GuardedBy("monitor")
+ @Nullable
+ private static Handler handler = null;
+
+ /**
+ * Creates and registers the Stackdriver Trace exporter to the OpenCensus library. Only one
+ * Stackdriver exporter can be registered at any point.
+ *
+ * <p>If the {@code credentials} in the provided {@link StackdriverTraceConfiguration} is not set,
+ * the exporter will use the default application credentials. See {@link
+ * GoogleCredentials#getApplicationDefault}.
+ *
+ * <p>If the {@code projectId} in the provided {@link StackdriverTraceConfiguration} is not set,
+ * the exporter will use the default project ID. See {@link ServiceOptions#getDefaultProjectId}.
+ *
+ * @param configuration the {@code StackdriverTraceConfiguration} used to create the exporter.
+ * @throws IllegalStateException if a Stackdriver exporter is already registered.
+ * @since 0.12
+ */
+ public static void createAndRegister(StackdriverTraceConfiguration configuration)
+ throws IOException {
+ synchronized (monitor) {
+ checkState(handler == null, "Stackdriver exporter is already registered.");
+ Credentials credentials = configuration.getCredentials();
+ String projectId = configuration.getProjectId();
+ projectId = projectId != null ? projectId : ServiceOptions.getDefaultProjectId();
+
+ StackdriverV2ExporterHandler handler;
+ TraceServiceStub stub = configuration.getTraceServiceStub();
+ if (stub == null) {
+ handler =
+ StackdriverV2ExporterHandler.createWithCredentials(
+ credentials != null ? credentials : GoogleCredentials.getApplicationDefault(),
+ projectId);
+ } else {
+ handler = new StackdriverV2ExporterHandler(projectId, TraceServiceClient.create(stub));
+ }
+
+ registerInternal(handler);
+ }
+ }
+
+ private static void registerInternal(Handler newHandler) {
+ synchronized (monitor) {
+ handler = newHandler;
+ register(Tracing.getExportComponent().getSpanExporter(), newHandler);
+ }
+ }
+
+ /**
+ * Registers the {@code StackdriverTraceExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(SpanExporter spanExporter, Handler handler) {
+ spanExporter.registerHandler(REGISTER_NAME, handler);
+ }
+
+ /**
+ * Unregisters the Stackdriver Trace exporter from the OpenCensus library.
+ *
+ * @throws IllegalStateException if a Stackdriver exporter is not registered.
+ * @since 0.12
+ */
+ public static void unregister() {
+ synchronized (monitor) {
+ checkState(handler != null, "Stackdriver exporter is not registered.");
+ unregister(Tracing.getExportComponent().getSpanExporter());
+ handler = null;
+ }
+ }
+
+ /**
+ * Unregisters the {@code StackdriverTraceExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(SpanExporter spanExporter) {
+ spanExporter.unregisterHandler(REGISTER_NAME);
+ }
+
+ private StackdriverTraceExporter() {}
+}
diff --git a/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandler.java b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandler.java
new file mode 100644
index 00000000..de022c3f
--- /dev/null
+++ b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandler.java
@@ -0,0 +1,501 @@
+/*
+ * Copyright 2017, 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.stackdriver;
+
+import static com.google.api.client.util.Preconditions.checkNotNull;
+
+import com.google.api.gax.core.FixedCredentialsProvider;
+import com.google.auth.Credentials;
+import com.google.cloud.trace.v2.TraceServiceClient;
+import com.google.cloud.trace.v2.TraceServiceSettings;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.cloudtrace.v2.AttributeValue;
+import com.google.devtools.cloudtrace.v2.AttributeValue.Builder;
+import com.google.devtools.cloudtrace.v2.ProjectName;
+import com.google.devtools.cloudtrace.v2.Span;
+import com.google.devtools.cloudtrace.v2.Span.Attributes;
+import com.google.devtools.cloudtrace.v2.Span.Link;
+import com.google.devtools.cloudtrace.v2.Span.Links;
+import com.google.devtools.cloudtrace.v2.Span.TimeEvent;
+import com.google.devtools.cloudtrace.v2.Span.TimeEvent.MessageEvent;
+import com.google.devtools.cloudtrace.v2.SpanName;
+import com.google.devtools.cloudtrace.v2.TruncatableString;
+import com.google.protobuf.Int32Value;
+import com.google.rpc.Status;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.OpenCensusLibraryInformation;
+import io.opencensus.common.Scope;
+import io.opencensus.common.Timestamp;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResourceUtils;
+import io.opencensus.contrib.monitoredresource.util.ResourceType;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.MessageEvent.Type;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.samplers.Samplers;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Exporter to Stackdriver Trace API v2. */
+final class StackdriverV2ExporterHandler extends SpanExporter.Handler {
+
+ private static final Tracer tracer = Tracing.getTracer();
+ private static final Sampler probabilitySampler = Samplers.probabilitySampler(0.0001);
+ private static final String AGENT_LABEL_KEY = "g.co/agent";
+ private static final String AGENT_LABEL_VALUE_STRING =
+ "opencensus-java [" + OpenCensusLibraryInformation.VERSION + "]";
+ private static final String SERVER_PREFIX = "Recv.";
+ private static final String CLIENT_PREFIX = "Sent.";
+ private static final AttributeValue AGENT_LABEL_VALUE =
+ AttributeValue.newBuilder()
+ .setStringValue(toTruncatableStringProto(AGENT_LABEL_VALUE_STRING))
+ .build();
+
+ private static final ImmutableMap<String, String> HTTP_ATTRIBUTE_MAPPING =
+ ImmutableMap.<String, String>builder()
+ .put("http.host", "/http/host")
+ .put("http.method", "/http/method")
+ .put("http.path", "/http/path")
+ .put("http.route", "/http/route")
+ .put("http.user_agent", "/http/user_agent")
+ .put("http.status_code", "/http/status_code")
+ .build();
+
+ @javax.annotation.Nullable
+ private static final MonitoredResource RESOURCE = MonitoredResourceUtils.getDefaultResource();
+
+ // Only initialize once.
+ private static final Map<String, AttributeValue> RESOURCE_LABELS = getResourceLabels(RESOURCE);
+
+ // Constant functions for AttributeValue.
+ private static final Function<String, /*@Nullable*/ AttributeValue> stringAttributeValueFunction =
+ new Function<String, /*@Nullable*/ AttributeValue>() {
+ @Override
+ public AttributeValue apply(String stringValue) {
+ Builder attributeValueBuilder = AttributeValue.newBuilder();
+ attributeValueBuilder.setStringValue(toTruncatableStringProto(stringValue));
+ return attributeValueBuilder.build();
+ }
+ };
+ private static final Function<Boolean, /*@Nullable*/ AttributeValue>
+ booleanAttributeValueFunction =
+ new Function<Boolean, /*@Nullable*/ AttributeValue>() {
+ @Override
+ public AttributeValue apply(Boolean booleanValue) {
+ Builder attributeValueBuilder = AttributeValue.newBuilder();
+ attributeValueBuilder.setBoolValue(booleanValue);
+ return attributeValueBuilder.build();
+ }
+ };
+ private static final Function<Long, /*@Nullable*/ AttributeValue> longAttributeValueFunction =
+ new Function<Long, /*@Nullable*/ AttributeValue>() {
+ @Override
+ public AttributeValue apply(Long longValue) {
+ Builder attributeValueBuilder = AttributeValue.newBuilder();
+ attributeValueBuilder.setIntValue(longValue);
+ return attributeValueBuilder.build();
+ }
+ };
+ private static final Function<Double, /*@Nullable*/ AttributeValue> doubleAttributeValueFunction =
+ new Function<Double, /*@Nullable*/ AttributeValue>() {
+ @Override
+ public AttributeValue apply(Double doubleValue) {
+ Builder attributeValueBuilder = AttributeValue.newBuilder();
+ // TODO: set double value if Stackdriver Trace support it in the future.
+ attributeValueBuilder.setStringValue(
+ toTruncatableStringProto(String.valueOf(doubleValue)));
+ return attributeValueBuilder.build();
+ }
+ };
+
+ private final String projectId;
+ private final TraceServiceClient traceServiceClient;
+ private final ProjectName projectName;
+
+ @VisibleForTesting
+ StackdriverV2ExporterHandler(String projectId, TraceServiceClient traceServiceClient) {
+ this.projectId = checkNotNull(projectId, "projectId");
+ this.traceServiceClient = traceServiceClient;
+ projectName = ProjectName.of(this.projectId);
+
+ Tracing.getExportComponent()
+ .getSampledSpanStore()
+ .registerSpanNamesForCollection(Collections.singletonList("ExportStackdriverTraces"));
+ }
+
+ static StackdriverV2ExporterHandler createWithCredentials(
+ Credentials credentials, String projectId) throws IOException {
+ checkNotNull(credentials, "credentials");
+ TraceServiceSettings traceServiceSettings =
+ TraceServiceSettings.newBuilder()
+ .setCredentialsProvider(FixedCredentialsProvider.create(credentials))
+ .build();
+ return new StackdriverV2ExporterHandler(
+ projectId, TraceServiceClient.create(traceServiceSettings));
+ }
+
+ @VisibleForTesting
+ Span generateSpan(SpanData spanData, Map<String, AttributeValue> resourceLabels) {
+ SpanContext context = spanData.getContext();
+ final String spanIdHex = context.getSpanId().toLowerBase16();
+ SpanName spanName =
+ SpanName.newBuilder()
+ .setProject(projectId)
+ .setTrace(context.getTraceId().toLowerBase16())
+ .setSpan(spanIdHex)
+ .build();
+ Span.Builder spanBuilder =
+ Span.newBuilder()
+ .setName(spanName.toString())
+ .setSpanId(spanIdHex)
+ .setDisplayName(
+ toTruncatableStringProto(toDisplayName(spanData.getName(), spanData.getKind())))
+ .setStartTime(toTimestampProto(spanData.getStartTimestamp()))
+ .setAttributes(toAttributesProto(spanData.getAttributes(), resourceLabels))
+ .setTimeEvents(
+ toTimeEventsProto(spanData.getAnnotations(), spanData.getMessageEvents()));
+ io.opencensus.trace.Status status = spanData.getStatus();
+ if (status != null) {
+ spanBuilder.setStatus(toStatusProto(status));
+ }
+ Timestamp end = spanData.getEndTimestamp();
+ if (end != null) {
+ spanBuilder.setEndTime(toTimestampProto(end));
+ }
+ spanBuilder.setLinks(toLinksProto(spanData.getLinks()));
+ Integer childSpanCount = spanData.getChildSpanCount();
+ if (childSpanCount != null) {
+ spanBuilder.setChildSpanCount(Int32Value.newBuilder().setValue(childSpanCount).build());
+ }
+ if (spanData.getParentSpanId() != null && spanData.getParentSpanId().isValid()) {
+ spanBuilder.setParentSpanId(spanData.getParentSpanId().toLowerBase16());
+ }
+
+ return spanBuilder.build();
+ }
+
+ private static Span.TimeEvents toTimeEventsProto(
+ TimedEvents<Annotation> annotationTimedEvents,
+ TimedEvents<io.opencensus.trace.MessageEvent> messageEventTimedEvents) {
+ Span.TimeEvents.Builder timeEventsBuilder = Span.TimeEvents.newBuilder();
+ timeEventsBuilder.setDroppedAnnotationsCount(annotationTimedEvents.getDroppedEventsCount());
+ for (TimedEvent<Annotation> annotation : annotationTimedEvents.getEvents()) {
+ timeEventsBuilder.addTimeEvent(toTimeAnnotationProto(annotation));
+ }
+ timeEventsBuilder.setDroppedMessageEventsCount(messageEventTimedEvents.getDroppedEventsCount());
+ for (TimedEvent<io.opencensus.trace.MessageEvent> networkEvent :
+ messageEventTimedEvents.getEvents()) {
+ timeEventsBuilder.addTimeEvent(toTimeMessageEventProto(networkEvent));
+ }
+ return timeEventsBuilder.build();
+ }
+
+ private static TimeEvent toTimeAnnotationProto(TimedEvent<Annotation> timedEvent) {
+ TimeEvent.Builder timeEventBuilder =
+ TimeEvent.newBuilder().setTime(toTimestampProto(timedEvent.getTimestamp()));
+ Annotation annotation = timedEvent.getEvent();
+ timeEventBuilder.setAnnotation(
+ TimeEvent.Annotation.newBuilder()
+ .setDescription(toTruncatableStringProto(annotation.getDescription()))
+ .setAttributes(toAttributesBuilderProto(annotation.getAttributes(), 0))
+ .build());
+ return timeEventBuilder.build();
+ }
+
+ private static TimeEvent toTimeMessageEventProto(
+ TimedEvent<io.opencensus.trace.MessageEvent> timedEvent) {
+ TimeEvent.Builder timeEventBuilder =
+ TimeEvent.newBuilder().setTime(toTimestampProto(timedEvent.getTimestamp()));
+ io.opencensus.trace.MessageEvent messageEvent = timedEvent.getEvent();
+ timeEventBuilder.setMessageEvent(
+ TimeEvent.MessageEvent.newBuilder()
+ .setId(messageEvent.getMessageId())
+ .setCompressedSizeBytes(messageEvent.getCompressedMessageSize())
+ .setUncompressedSizeBytes(messageEvent.getUncompressedMessageSize())
+ .setType(toMessageEventTypeProto(messageEvent))
+ .build());
+ return timeEventBuilder.build();
+ }
+
+ private static TimeEvent.MessageEvent.Type toMessageEventTypeProto(
+ io.opencensus.trace.MessageEvent messageEvent) {
+ if (messageEvent.getType() == Type.RECEIVED) {
+ return MessageEvent.Type.RECEIVED;
+ } else {
+ return MessageEvent.Type.SENT;
+ }
+ }
+
+ // These are the attributes of the Span, where usually we may add more attributes like the agent.
+ private static Attributes toAttributesProto(
+ io.opencensus.trace.export.SpanData.Attributes attributes,
+ Map<String, AttributeValue> resourceLabels) {
+ Attributes.Builder attributesBuilder =
+ toAttributesBuilderProto(
+ attributes.getAttributeMap(), attributes.getDroppedAttributesCount());
+ attributesBuilder.putAttributeMap(AGENT_LABEL_KEY, AGENT_LABEL_VALUE);
+ for (Entry<String, AttributeValue> entry : resourceLabels.entrySet()) {
+ attributesBuilder.putAttributeMap(entry.getKey(), entry.getValue());
+ }
+ return attributesBuilder.build();
+ }
+
+ private static Attributes.Builder toAttributesBuilderProto(
+ Map<String, io.opencensus.trace.AttributeValue> attributes, int droppedAttributesCount) {
+ Attributes.Builder attributesBuilder =
+ Attributes.newBuilder().setDroppedAttributesCount(droppedAttributesCount);
+ for (Map.Entry<String, io.opencensus.trace.AttributeValue> label : attributes.entrySet()) {
+ AttributeValue value = toAttributeValueProto(label.getValue());
+ if (value != null) {
+ attributesBuilder.putAttributeMap(mapKey(label.getKey()), value);
+ }
+ }
+ return attributesBuilder;
+ }
+
+ @VisibleForTesting
+ static Map<String, AttributeValue> getResourceLabels(
+ @javax.annotation.Nullable MonitoredResource resource) {
+ if (resource == null) {
+ return Collections.emptyMap();
+ }
+ Map<String, AttributeValue> resourceLabels = new HashMap<String, AttributeValue>();
+ ResourceType resourceType = resource.getResourceType();
+ switch (resourceType) {
+ case AWS_EC2_INSTANCE:
+ AwsEc2InstanceMonitoredResource awsEc2InstanceMonitoredResource =
+ (AwsEc2InstanceMonitoredResource) resource;
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "aws_account",
+ awsEc2InstanceMonitoredResource.getAccount());
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "instance_id",
+ awsEc2InstanceMonitoredResource.getInstanceId());
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "region",
+ "aws:" + awsEc2InstanceMonitoredResource.getRegion());
+ return Collections.unmodifiableMap(resourceLabels);
+ case GCP_GCE_INSTANCE:
+ GcpGceInstanceMonitoredResource gcpGceInstanceMonitoredResource =
+ (GcpGceInstanceMonitoredResource) resource;
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "project_id",
+ gcpGceInstanceMonitoredResource.getAccount());
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "instance_id",
+ gcpGceInstanceMonitoredResource.getInstanceId());
+ putToResourceAttributeMap(
+ resourceLabels, resourceType, "zone", gcpGceInstanceMonitoredResource.getZone());
+ return Collections.unmodifiableMap(resourceLabels);
+ case GCP_GKE_CONTAINER:
+ GcpGkeContainerMonitoredResource gcpGkeContainerMonitoredResource =
+ (GcpGkeContainerMonitoredResource) resource;
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "project_id",
+ gcpGkeContainerMonitoredResource.getAccount());
+ putToResourceAttributeMap(
+ resourceLabels, resourceType, "location", gcpGkeContainerMonitoredResource.getZone());
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "cluster_name",
+ gcpGkeContainerMonitoredResource.getClusterName());
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "container_name",
+ gcpGkeContainerMonitoredResource.getContainerName());
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "namespace_name",
+ gcpGkeContainerMonitoredResource.getNamespaceId());
+ putToResourceAttributeMap(
+ resourceLabels, resourceType, "pod_name", gcpGkeContainerMonitoredResource.getPodId());
+ return Collections.unmodifiableMap(resourceLabels);
+ }
+ return Collections.emptyMap();
+ }
+
+ private static void putToResourceAttributeMap(
+ Map<String, AttributeValue> map,
+ ResourceType resourceType,
+ String attributeName,
+ String attributeValue) {
+ map.put(
+ createResourceLabelKey(resourceType, attributeName),
+ toStringAttributeValueProto(attributeValue));
+ }
+
+ @VisibleForTesting
+ static String createResourceLabelKey(ResourceType resourceType, String resourceAttribute) {
+ return String.format("g.co/r/%s/%s", mapToStringResourceType(resourceType), resourceAttribute);
+ }
+
+ private static String mapToStringResourceType(ResourceType resourceType) {
+ switch (resourceType) {
+ case GCP_GCE_INSTANCE:
+ return "gce_instance";
+ case GCP_GKE_CONTAINER:
+ return "k8s_container";
+ case AWS_EC2_INSTANCE:
+ return "aws_ec2_instance";
+ }
+ throw new IllegalArgumentException("Unknown resource type.");
+ }
+
+ @VisibleForTesting
+ static AttributeValue toStringAttributeValueProto(String value) {
+ return AttributeValue.newBuilder().setStringValue(toTruncatableStringProto(value)).build();
+ }
+
+ private static String mapKey(String key) {
+ if (HTTP_ATTRIBUTE_MAPPING.containsKey(key)) {
+ return HTTP_ATTRIBUTE_MAPPING.get(key);
+ } else {
+ return key;
+ }
+ }
+
+ private static Status toStatusProto(io.opencensus.trace.Status status) {
+ Status.Builder statusBuilder = Status.newBuilder().setCode(status.getCanonicalCode().value());
+ if (status.getDescription() != null) {
+ statusBuilder.setMessage(status.getDescription());
+ }
+ return statusBuilder.build();
+ }
+
+ private static TruncatableString toTruncatableStringProto(String string) {
+ return TruncatableString.newBuilder().setValue(string).setTruncatedByteCount(0).build();
+ }
+
+ private static com.google.protobuf.Timestamp toTimestampProto(Timestamp timestamp) {
+ return com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(timestamp.getSeconds())
+ .setNanos(timestamp.getNanos())
+ .build();
+ }
+
+ @javax.annotation.Nullable
+ private static AttributeValue toAttributeValueProto(
+ io.opencensus.trace.AttributeValue attributeValue) {
+ return attributeValue.match(
+ stringAttributeValueFunction,
+ booleanAttributeValueFunction,
+ longAttributeValueFunction,
+ doubleAttributeValueFunction,
+ Functions.</*@Nullable*/ AttributeValue>returnNull());
+ }
+
+ private static Link.Type toLinkTypeProto(io.opencensus.trace.Link.Type type) {
+ if (type == io.opencensus.trace.Link.Type.PARENT_LINKED_SPAN) {
+ return Link.Type.PARENT_LINKED_SPAN;
+ } else {
+ return Link.Type.CHILD_LINKED_SPAN;
+ }
+ }
+
+ private static String toDisplayName(String spanName, @javax.annotation.Nullable Kind spanKind) {
+ if (spanKind == Kind.SERVER && !spanName.startsWith(SERVER_PREFIX)) {
+ return SERVER_PREFIX + spanName;
+ }
+
+ if (spanKind == Kind.CLIENT && !spanName.startsWith(CLIENT_PREFIX)) {
+ return CLIENT_PREFIX + spanName;
+ }
+
+ return spanName;
+ }
+
+ private static Link toLinkProto(io.opencensus.trace.Link link) {
+ checkNotNull(link);
+ return Link.newBuilder()
+ .setTraceId(link.getTraceId().toLowerBase16())
+ .setSpanId(link.getSpanId().toLowerBase16())
+ .setType(toLinkTypeProto(link.getType()))
+ .setAttributes(toAttributesBuilderProto(link.getAttributes(), 0))
+ .build();
+ }
+
+ private static Links toLinksProto(io.opencensus.trace.export.SpanData.Links links) {
+ final Links.Builder linksBuilder =
+ Links.newBuilder().setDroppedLinksCount(links.getDroppedLinksCount());
+ for (io.opencensus.trace.Link link : links.getLinks()) {
+ linksBuilder.addLink(toLinkProto(link));
+ }
+ return linksBuilder.build();
+ }
+
+ @Override
+ public void export(Collection<SpanData> spanDataList) {
+ // Start a new span with explicit 1/10000 sampling probability to avoid the case when user
+ // sets the default sampler to always sample and we get the gRPC span of the stackdriver
+ // export call always sampled and go to an infinite loop.
+ Scope scope =
+ tracer
+ .spanBuilder("ExportStackdriverTraces")
+ .setSampler(probabilitySampler)
+ .setRecordEvents(true)
+ .startScopedSpan();
+ try {
+ List<Span> spans = new ArrayList<>(spanDataList.size());
+ for (SpanData spanData : spanDataList) {
+ spans.add(generateSpan(spanData, RESOURCE_LABELS));
+ }
+ // Sync call because it is already called for a batch of data, and on a separate thread.
+ // TODO(bdrutu): Consider to make this async in the future.
+ traceServiceClient.batchWriteSpans(projectName, spans);
+ } finally {
+ scope.close();
+ }
+ }
+}
diff --git a/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfigurationTest.java b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfigurationTest.java
new file mode 100644
index 00000000..6926e869
--- /dev/null
+++ b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfigurationTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.GoogleCredentials;
+import java.util.Date;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link StackdriverTraceConfiguration}. */
+@RunWith(JUnit4.class)
+public class StackdriverTraceConfigurationTest {
+
+ private static final Credentials FAKE_CREDENTIALS =
+ GoogleCredentials.newBuilder().setAccessToken(new AccessToken("fake", new Date(100))).build();
+ private static final String PROJECT_ID = "project";
+
+ @Test
+ public void defaultConfiguration() {
+ StackdriverTraceConfiguration configuration = StackdriverTraceConfiguration.builder().build();
+ assertThat(configuration.getCredentials()).isNull();
+ assertThat(configuration.getProjectId()).isNull();
+ }
+
+ @Test
+ public void updateAll() {
+ StackdriverTraceConfiguration configuration =
+ StackdriverTraceConfiguration.builder()
+ .setCredentials(FAKE_CREDENTIALS)
+ .setProjectId(PROJECT_ID)
+ .build();
+ assertThat(configuration.getCredentials()).isEqualTo(FAKE_CREDENTIALS);
+ assertThat(configuration.getProjectId()).isEqualTo(PROJECT_ID);
+ }
+}
diff --git a/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporterTest.java b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporterTest.java
new file mode 100644
index 00000000..6a12a899
--- /dev/null
+++ b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporterTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017, 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.stackdriver;
+
+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 io.opencensus.trace.export.SpanExporter.Handler;
+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;
+
+/** Unit tests for {@link StackdriverTraceExporter}. */
+@RunWith(JUnit4.class)
+public class StackdriverTraceExporterTest {
+ @Mock private SpanExporter spanExporter;
+ @Mock private Handler handler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void registerUnregisterStackdriverExporter() {
+ StackdriverTraceExporter.register(spanExporter, handler);
+ verify(spanExporter)
+ .registerHandler(
+ eq("io.opencensus.exporter.trace.stackdriver.StackdriverTraceExporter"), same(handler));
+ StackdriverTraceExporter.unregister(spanExporter);
+ verify(spanExporter)
+ .unregisterHandler(eq("io.opencensus.exporter.trace.stackdriver.StackdriverTraceExporter"));
+ }
+}
diff --git a/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerExportTest.java b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerExportTest.java
new file mode 100644
index 00000000..32458597
--- /dev/null
+++ b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerExportTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.stackdriver;
+
+import static org.mockito.Mockito.when;
+
+import com.google.cloud.trace.v2.TraceServiceClient;
+import com.google.cloud.trace.v2.stub.TraceServiceStub;
+import io.opencensus.trace.export.SpanData;
+import java.util.Collection;
+import java.util.Collections;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for exporting in {@link StackdriverV2ExporterHandler}. */
+@RunWith(JUnit4.class)
+public final class StackdriverV2ExporterHandlerExportTest {
+ private static final String PROJECT_ID = "PROJECT_ID";
+ // mock the service stub to provide a fake trace service.
+ @Mock private TraceServiceStub traceServiceStub;
+ private TraceServiceClient traceServiceClient;
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ private StackdriverV2ExporterHandler handler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ // TODO(@Hailong): TraceServiceClient.create(TraceServiceStub) is a beta API and might change
+ // in the future.
+ traceServiceClient = TraceServiceClient.create(traceServiceStub);
+ handler = new StackdriverV2ExporterHandler(PROJECT_ID, traceServiceClient);
+ }
+
+ @Test
+ public void export() {
+ when(traceServiceStub.batchWriteSpansCallable())
+ .thenThrow(new RuntimeException("TraceServiceStub called"));
+ Collection<SpanData> spanDataList = Collections.<SpanData>emptyList();
+ thrown.expect(RuntimeException.class);
+ thrown.expectMessage("TraceServiceStub called");
+ handler.export(spanDataList);
+ }
+}
diff --git a/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerProtoTest.java b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerProtoTest.java
new file mode 100644
index 00000000..8b28dc06
--- /dev/null
+++ b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerProtoTest.java
@@ -0,0 +1,489 @@
+/*
+ * 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.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.contrib.monitoredresource.util.ResourceType.AWS_EC2_INSTANCE;
+import static io.opencensus.contrib.monitoredresource.util.ResourceType.GCP_GCE_INSTANCE;
+import static io.opencensus.contrib.monitoredresource.util.ResourceType.GCP_GKE_CONTAINER;
+import static io.opencensus.exporter.trace.stackdriver.StackdriverV2ExporterHandler.createResourceLabelKey;
+import static io.opencensus.exporter.trace.stackdriver.StackdriverV2ExporterHandler.toStringAttributeValueProto;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.cloudtrace.v2.AttributeValue;
+import com.google.devtools.cloudtrace.v2.Span;
+import com.google.devtools.cloudtrace.v2.Span.TimeEvent;
+import com.google.devtools.cloudtrace.v2.Span.TimeEvent.MessageEvent;
+import com.google.devtools.cloudtrace.v2.StackTrace;
+import com.google.devtools.cloudtrace.v2.TruncatableString;
+import com.google.protobuf.Int32Value;
+import io.opencensus.common.Timestamp;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Span.Kind;
+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 io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for proto conversions in {@link StackdriverV2ExporterHandler}. */
+@RunWith(JUnit4.class)
+public final class StackdriverV2ExporterHandlerProtoTest {
+
+ private static final Credentials FAKE_CREDENTIALS =
+ GoogleCredentials.newBuilder().setAccessToken(new AccessToken("fake", new Date(100))).build();
+ // OpenCensus constants
+ private static final Timestamp startTimestamp = Timestamp.create(123, 456);
+ private static final Timestamp eventTimestamp1 = Timestamp.create(123, 457);
+ private static final Timestamp eventTimestamp2 = Timestamp.create(123, 458);
+ private static final Timestamp eventTimestamp3 = Timestamp.create(123, 459);
+ private static final Timestamp endTimestamp = Timestamp.create(123, 460);
+
+ private static final String PROJECT_ID = "PROJECT_ID";
+ private static final String TRACE_ID = "4bf92f3577b34da6a3ce929d0e0e4736";
+ private static final String SPAN_ID = "24aa0b2d371f48c9";
+ private static final String PARENT_SPAN_ID = "71da8d631536f5f1";
+ private static final String SPAN_NAME = "MySpanName";
+ private static final String SD_SPAN_NAME =
+ String.format("projects/%s/traces/%s/spans/%s", PROJECT_ID, TRACE_ID, SPAN_ID);
+ private static final String ANNOTATION_TEXT = "MyAnnotationText";
+ private static final String ATTRIBUTE_KEY_1 = "MyAttributeKey1";
+ private static final String ATTRIBUTE_KEY_2 = "MyAttributeKey2";
+
+ private static final int DROPPED_ATTRIBUTES_COUNT = 1;
+ private static final int DROPPED_ANNOTATIONS_COUNT = 2;
+ private static final int DROPPED_NETWORKEVENTS_COUNT = 3;
+ private static final int DROPPED_LINKS_COUNT = 4;
+ private static final int CHILD_SPAN_COUNT = 13;
+
+ private static final Annotation annotation = Annotation.fromDescription(ANNOTATION_TEXT);
+ private static final io.opencensus.trace.MessageEvent recvMessageEvent =
+ io.opencensus.trace.MessageEvent.builder(io.opencensus.trace.MessageEvent.Type.RECEIVED, 1)
+ .build();
+ private static final io.opencensus.trace.MessageEvent sentMessageEvent =
+ io.opencensus.trace.MessageEvent.builder(io.opencensus.trace.MessageEvent.Type.SENT, 1)
+ .build();
+ private static final Status status = Status.DEADLINE_EXCEEDED.withDescription("TooSlow");
+ private static final SpanId parentSpanId = SpanId.fromLowerBase16(PARENT_SPAN_ID);
+ private static final SpanId spanId = SpanId.fromLowerBase16(SPAN_ID);
+ private static final TraceId traceId = TraceId.fromLowerBase16(TRACE_ID);
+ private static final TraceOptions traceOptions = TraceOptions.DEFAULT;
+ private static final SpanContext spanContext = SpanContext.create(traceId, spanId, traceOptions);
+
+ private static final List<TimedEvent<Annotation>> annotationsList =
+ ImmutableList.of(
+ SpanData.TimedEvent.create(eventTimestamp1, annotation),
+ SpanData.TimedEvent.create(eventTimestamp3, annotation));
+ private static final List<TimedEvent<io.opencensus.trace.MessageEvent>> networkEventsList =
+ ImmutableList.of(
+ SpanData.TimedEvent.create(eventTimestamp1, recvMessageEvent),
+ SpanData.TimedEvent.create(eventTimestamp2, sentMessageEvent));
+ private static final List<Link> linksList =
+ ImmutableList.of(Link.fromSpanContext(spanContext, Link.Type.CHILD_LINKED_SPAN));
+
+ private static final SpanData.Attributes attributes =
+ SpanData.Attributes.create(
+ ImmutableMap.of(
+ ATTRIBUTE_KEY_1,
+ io.opencensus.trace.AttributeValue.longAttributeValue(10L),
+ ATTRIBUTE_KEY_2,
+ io.opencensus.trace.AttributeValue.booleanAttributeValue(true)),
+ DROPPED_ATTRIBUTES_COUNT);
+ private static final TimedEvents<Annotation> annotations =
+ TimedEvents.create(annotationsList, DROPPED_ANNOTATIONS_COUNT);
+ private static final TimedEvents<io.opencensus.trace.MessageEvent> messageEvents =
+ TimedEvents.create(networkEventsList, DROPPED_NETWORKEVENTS_COUNT);
+ private static final SpanData.Links links = SpanData.Links.create(linksList, DROPPED_LINKS_COUNT);
+ private static final Map<String, AttributeValue> EMPTY_RESOURCE_LABELS = Collections.emptyMap();
+ private static final AwsEc2InstanceMonitoredResource AWS_EC2_INSTANCE_MONITORED_RESOURCE =
+ AwsEc2InstanceMonitoredResource.create("my-project", "my-instance", "us-east-1");
+ private static final GcpGceInstanceMonitoredResource GCP_GCE_INSTANCE_MONITORED_RESOURCE =
+ GcpGceInstanceMonitoredResource.create("my-project", "my-instance", "us-east1");
+ private static final GcpGkeContainerMonitoredResource GCP_GKE_CONTAINER_MONITORED_RESOURCE =
+ GcpGkeContainerMonitoredResource.create(
+ "my-project", "cluster", "container", "namespace", "my-instance", "pod", "us-east1");
+ private static final ImmutableMap<String, AttributeValue> AWS_RESOURCE_LABELS =
+ ImmutableMap.of(
+ createResourceLabelKey(AWS_EC2_INSTANCE, "aws_account"),
+ toStringAttributeValueProto("my-project"),
+ createResourceLabelKey(AWS_EC2_INSTANCE, "instance_id"),
+ toStringAttributeValueProto("my-instance"),
+ createResourceLabelKey(AWS_EC2_INSTANCE, "region"),
+ toStringAttributeValueProto("aws:us-east-1"));
+ private static final ImmutableMap<String, AttributeValue> GCE_RESOURCE_LABELS =
+ ImmutableMap.of(
+ createResourceLabelKey(GCP_GCE_INSTANCE, "project_id"),
+ toStringAttributeValueProto("my-project"),
+ createResourceLabelKey(GCP_GCE_INSTANCE, "instance_id"),
+ toStringAttributeValueProto("my-instance"),
+ createResourceLabelKey(GCP_GCE_INSTANCE, "zone"),
+ toStringAttributeValueProto("us-east1"));
+ private static final ImmutableMap<String, AttributeValue> GKE_RESOURCE_LABELS =
+ ImmutableMap.<String, AttributeValue>builder()
+ .put(
+ createResourceLabelKey(GCP_GKE_CONTAINER, "project_id"),
+ toStringAttributeValueProto("my-project"))
+ .put(
+ createResourceLabelKey(GCP_GKE_CONTAINER, "cluster_name"),
+ toStringAttributeValueProto("cluster"))
+ .put(
+ createResourceLabelKey(GCP_GKE_CONTAINER, "container_name"),
+ toStringAttributeValueProto("container"))
+ .put(
+ createResourceLabelKey(GCP_GKE_CONTAINER, "namespace_name"),
+ toStringAttributeValueProto("namespace"))
+ .put(
+ createResourceLabelKey(GCP_GKE_CONTAINER, "pod_name"),
+ toStringAttributeValueProto("pod"))
+ .put(
+ createResourceLabelKey(GCP_GKE_CONTAINER, "location"),
+ toStringAttributeValueProto("us-east1"))
+ .build();
+
+ private StackdriverV2ExporterHandler handler;
+
+ @Before
+ public void setUp() throws IOException {
+ handler = StackdriverV2ExporterHandler.createWithCredentials(FAKE_CREDENTIALS, PROJECT_ID);
+ }
+
+ @Test
+ public void generateSpan() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ /* hasRemoteParent= */ true,
+ SPAN_NAME,
+ null,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ TimeEvent annotationTimeEvent1 =
+ TimeEvent.newBuilder()
+ .setAnnotation(
+ TimeEvent.Annotation.newBuilder()
+ .setDescription(
+ TruncatableString.newBuilder().setValue(ANNOTATION_TEXT).build())
+ .setAttributes(Span.Attributes.newBuilder().build())
+ .build())
+ .setTime(
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(eventTimestamp1.getSeconds())
+ .setNanos(eventTimestamp1.getNanos())
+ .build())
+ .build();
+ TimeEvent annotationTimeEvent2 =
+ TimeEvent.newBuilder()
+ .setAnnotation(
+ TimeEvent.Annotation.newBuilder()
+ .setDescription(
+ TruncatableString.newBuilder().setValue(ANNOTATION_TEXT).build())
+ .setAttributes(Span.Attributes.newBuilder().build())
+ .build())
+ .setTime(
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(eventTimestamp3.getSeconds())
+ .setNanos(eventTimestamp3.getNanos())
+ .build())
+ .build();
+
+ TimeEvent sentTimeEvent =
+ TimeEvent.newBuilder()
+ .setMessageEvent(
+ TimeEvent.MessageEvent.newBuilder()
+ .setType(MessageEvent.Type.SENT)
+ .setId(sentMessageEvent.getMessageId()))
+ .setTime(
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(eventTimestamp2.getSeconds())
+ .setNanos(eventTimestamp2.getNanos())
+ .build())
+ .build();
+ TimeEvent recvTimeEvent =
+ TimeEvent.newBuilder()
+ .setMessageEvent(
+ TimeEvent.MessageEvent.newBuilder()
+ .setType(MessageEvent.Type.RECEIVED)
+ .setId(recvMessageEvent.getMessageId()))
+ .setTime(
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(eventTimestamp1.getSeconds())
+ .setNanos(eventTimestamp1.getNanos())
+ .build())
+ .build();
+
+ Span.Links spanLinks =
+ Span.Links.newBuilder()
+ .setDroppedLinksCount(DROPPED_LINKS_COUNT)
+ .addLink(
+ Span.Link.newBuilder()
+ .setType(Span.Link.Type.CHILD_LINKED_SPAN)
+ .setTraceId(TRACE_ID)
+ .setSpanId(SPAN_ID)
+ .setAttributes(Span.Attributes.newBuilder().build())
+ .build())
+ .build();
+
+ com.google.rpc.Status spanStatus =
+ com.google.rpc.Status.newBuilder()
+ .setCode(com.google.rpc.Code.DEADLINE_EXCEEDED.getNumber())
+ .setMessage("TooSlow")
+ .build();
+
+ com.google.protobuf.Timestamp startTime =
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(startTimestamp.getSeconds())
+ .setNanos(startTimestamp.getNanos())
+ .build();
+ com.google.protobuf.Timestamp endTime =
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(endTimestamp.getSeconds())
+ .setNanos(endTimestamp.getNanos())
+ .build();
+
+ Span span = handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS);
+ assertThat(span.getName()).isEqualTo(SD_SPAN_NAME);
+ assertThat(span.getSpanId()).isEqualTo(SPAN_ID);
+ assertThat(span.getParentSpanId()).isEqualTo(PARENT_SPAN_ID);
+ assertThat(span.getDisplayName())
+ .isEqualTo(TruncatableString.newBuilder().setValue(SPAN_NAME).build());
+ assertThat(span.getStartTime()).isEqualTo(startTime);
+ assertThat(span.getEndTime()).isEqualTo(endTime);
+ assertThat(span.getAttributes().getDroppedAttributesCount())
+ .isEqualTo(DROPPED_ATTRIBUTES_COUNT);
+ // The generated attributes map contains more values (e.g. agent). We only test what we added.
+ assertThat(span.getAttributes().getAttributeMapMap())
+ .containsEntry(ATTRIBUTE_KEY_1, AttributeValue.newBuilder().setIntValue(10L).build());
+ assertThat(span.getAttributes().getAttributeMapMap())
+ .containsEntry(ATTRIBUTE_KEY_2, AttributeValue.newBuilder().setBoolValue(true).build());
+ // TODO(@Hailong): add stack trace test in the future.
+ assertThat(span.getStackTrace()).isEqualTo(StackTrace.newBuilder().build());
+ assertThat(span.getTimeEvents().getDroppedMessageEventsCount())
+ .isEqualTo(DROPPED_NETWORKEVENTS_COUNT);
+ assertThat(span.getTimeEvents().getDroppedAnnotationsCount())
+ .isEqualTo(DROPPED_ANNOTATIONS_COUNT);
+ assertThat(span.getTimeEvents().getTimeEventList())
+ .containsAllOf(annotationTimeEvent1, annotationTimeEvent2, sentTimeEvent, recvTimeEvent);
+ assertThat(span.getLinks()).isEqualTo(spanLinks);
+ assertThat(span.getStatus()).isEqualTo(spanStatus);
+ assertThat(span.getSameProcessAsParentSpan())
+ .isEqualTo(com.google.protobuf.BoolValue.newBuilder().build());
+ assertThat(span.getChildSpanCount())
+ .isEqualTo(Int32Value.newBuilder().setValue(CHILD_SPAN_COUNT).build());
+ }
+
+ @Test
+ public void getResourceLabels_AwsEc2ResourceLabels() {
+ testGetResourceLabels(AWS_EC2_INSTANCE_MONITORED_RESOURCE, AWS_RESOURCE_LABELS);
+ }
+
+ @Test
+ public void getResourceLabels_GceResourceLabels() {
+ testGetResourceLabels(GCP_GCE_INSTANCE_MONITORED_RESOURCE, GCE_RESOURCE_LABELS);
+ }
+
+ @Test
+ public void getResourceLabels_GkeResourceLabels() {
+ testGetResourceLabels(GCP_GKE_CONTAINER_MONITORED_RESOURCE, GKE_RESOURCE_LABELS);
+ }
+
+ private static void testGetResourceLabels(
+ MonitoredResource resource, Map<String, AttributeValue> expectedLabels) {
+ Map<String, AttributeValue> actualLabels =
+ StackdriverV2ExporterHandler.getResourceLabels(resource);
+ assertThat(actualLabels).containsExactlyEntriesIn(expectedLabels);
+ }
+
+ @Test
+ public void generateSpan_WithResourceLabels() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ /* hasRemoteParent= */ true,
+ SPAN_NAME,
+ null,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ Span span = handler.generateSpan(spanData, AWS_RESOURCE_LABELS);
+ Map<String, AttributeValue> attributeMap = span.getAttributes().getAttributeMapMap();
+ assertThat(attributeMap.entrySet()).containsAllIn(AWS_RESOURCE_LABELS.entrySet());
+ }
+
+ @Test
+ public void mapHttpAttributes() {
+ Map<String, io.opencensus.trace.AttributeValue> attributesMap =
+ new HashMap<String, io.opencensus.trace.AttributeValue>();
+
+ attributesMap.put("http.host", io.opencensus.trace.AttributeValue.stringAttributeValue("host"));
+ attributesMap.put(
+ "http.method", io.opencensus.trace.AttributeValue.stringAttributeValue("method"));
+ attributesMap.put("http.path", io.opencensus.trace.AttributeValue.stringAttributeValue("path"));
+ attributesMap.put(
+ "http.route", io.opencensus.trace.AttributeValue.stringAttributeValue("route"));
+ attributesMap.put(
+ "http.user_agent", io.opencensus.trace.AttributeValue.stringAttributeValue("user_agent"));
+ attributesMap.put(
+ "http.status_code", io.opencensus.trace.AttributeValue.longAttributeValue(200L));
+ SpanData.Attributes httpAttributes = SpanData.Attributes.create(attributesMap, 0);
+
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ /* hasRemoteParent= */ true,
+ SPAN_NAME,
+ startTimestamp,
+ httpAttributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+
+ Span span = handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS);
+ Map<String, AttributeValue> attributes = span.getAttributes().getAttributeMapMap();
+
+ assertThat(attributes).containsEntry("/http/host", toStringAttributeValueProto("host"));
+ assertThat(attributes).containsEntry("/http/method", toStringAttributeValueProto("method"));
+ assertThat(attributes).containsEntry("/http/path", toStringAttributeValueProto("path"));
+ assertThat(attributes).containsEntry("/http/route", toStringAttributeValueProto("route"));
+ assertThat(attributes)
+ .containsEntry("/http/user_agent", toStringAttributeValueProto("user_agent"));
+ assertThat(attributes)
+ .containsEntry("/http/status_code", AttributeValue.newBuilder().setIntValue(200L).build());
+ }
+
+ @Test
+ public void generateSpanName_ForServer() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ /* hasRemoteParent= */ true,
+ SPAN_NAME,
+ Kind.SERVER,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue())
+ .isEqualTo("Recv." + SPAN_NAME);
+ }
+
+ @Test
+ public void generateSpanName_ForServerWithRecv() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ /* hasRemoteParent= */ true,
+ "Recv." + SPAN_NAME,
+ Kind.SERVER,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue())
+ .isEqualTo("Recv." + SPAN_NAME);
+ }
+
+ @Test
+ public void generateSpanName_ForClient() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ /* hasRemoteParent= */ true,
+ SPAN_NAME,
+ Kind.CLIENT,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue())
+ .isEqualTo("Sent." + SPAN_NAME);
+ }
+
+ @Test
+ public void generateSpanName_ForClientWithSent() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ /* hasRemoteParent= */ true,
+ "Sent." + SPAN_NAME,
+ Kind.CLIENT,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue())
+ .isEqualTo("Sent." + SPAN_NAME);
+ }
+}
diff --git a/exporters/trace/zipkin/README.md b/exporters/trace/zipkin/README.md
new file mode 100644
index 00000000..4398360d
--- /dev/null
+++ b/exporters/trace/zipkin/README.md
@@ -0,0 +1,82 @@
+# OpenCensus Zipkin Trace Exporter
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Zipkin Trace Exporter* is a trace exporter that exports
+data to Zipkin. [Zipkin](http://zipkin.io/) Zipkin is a distributed
+tracing system. It helps gather timing data needed to troubleshoot
+latency problems in microservice architectures. It manages both the
+collection and lookup of this data.
+
+## Quickstart
+
+### Prerequisites
+
+[Zipkin](http://zipkin.io/) stores and queries traces exported by
+applications instrumented with Census. The easiest way to start a zipkin
+server is to paste the below:
+
+```bash
+wget -O zipkin.jar 'https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec'
+java -jar zipkin.jar
+```
+
+
+### Hello Zipkin
+
+#### 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.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-trace-zipkin</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-trace-zipkin:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### Register the exporter
+
+This will report Zipkin v2 json format to a single server. Alternate
+[senders](https://github.com/openzipkin/zipkin-reporter-java) are available.
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) throws Exception {
+ ZipkinTraceExporter.createAndRegister("http://127.0.0.1:9411/api/v2/spans", "my-service");
+ // ...
+ }
+}
+```
+
+#### 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-zipkin/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-zipkin
diff --git a/exporters/trace/zipkin/build.gradle b/exporters/trace/zipkin/build.gradle
new file mode 100644
index 00000000..530dff7d
--- /dev/null
+++ b/exporters/trace/zipkin/build.gradle
@@ -0,0 +1,18 @@
+description = 'OpenCensus Trace Zipkin Exporter'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.6
+ it.targetCompatibility = 1.6
+}
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.guava,
+ libraries.zipkin_reporter,
+ libraries.zipkin_urlconnection
+
+ testCompile project(':opencensus-api')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporter.java b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporter.java
new file mode 100644
index 00000000..e20360e8
--- /dev/null
+++ b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporter.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2017, 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.zipkin;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import zipkin2.Span;
+import zipkin2.codec.SpanBytesEncoder;
+import zipkin2.reporter.Sender;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Zipkin.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * ZipkinExporter.createAndRegister("http://127.0.0.1:9411/api/v2/spans", "myservicename");
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @deprecated Deprecated due to inconsistent naming. Use {@link ZipkinTraceExporter}.
+ * @since 0.8
+ */
+@Deprecated
+public final class ZipkinExporter {
+
+ private ZipkinExporter() {}
+
+ /**
+ * Creates and registers the Zipkin Trace exporter to the OpenCensus library. Only one Zipkin
+ * exporter can be registered at any point.
+ *
+ * @param v2Url Ex http://127.0.0.1:9411/api/v2/spans
+ * @param serviceName the {@link Span#localServiceName() local service name} of the process.
+ * @throws IllegalStateException if a Zipkin exporter is already registered.
+ * @since 0.8
+ */
+ public static void createAndRegister(String v2Url, String serviceName) {
+ ZipkinTraceExporter.createAndRegister(v2Url, serviceName);
+ }
+
+ /**
+ * Creates and registers the Zipkin Trace exporter to the OpenCensus library. Only one Zipkin
+ * exporter can be registered at any point.
+ *
+ * @param encoder Usually {@link SpanBytesEncoder#JSON_V2}
+ * @param sender Often, but not necessarily an http sender. This could be Kafka or SQS.
+ * @param serviceName the {@link Span#localServiceName() local service name} of the process.
+ * @throws IllegalStateException if a Zipkin exporter is already registered.
+ * @since 0.8
+ */
+ public static void createAndRegister(
+ SpanBytesEncoder encoder, Sender sender, String serviceName) {
+ ZipkinTraceExporter.createAndRegister(encoder, sender, serviceName);
+ }
+
+ /**
+ * Registers the {@code ZipkinExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(SpanExporter spanExporter, Handler handler) {
+ ZipkinTraceExporter.register(spanExporter, handler);
+ }
+
+ /**
+ * Unregisters the Zipkin Trace exporter from the OpenCensus library.
+ *
+ * @throws IllegalStateException if a Zipkin exporter is not registered.
+ * @since 0.8
+ */
+ public static void unregister() {
+ ZipkinTraceExporter.unregister();
+ }
+
+ /**
+ * Unregisters the {@code ZipkinExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(SpanExporter spanExporter) {
+ ZipkinTraceExporter.unregister(spanExporter);
+ }
+}
diff --git a/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandler.java b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandler.java
new file mode 100644
index 00000000..70bc725c
--- /dev/null
+++ b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandler.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2017, 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.zipkin;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Scope;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.samplers.Samplers;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import zipkin2.Endpoint;
+import zipkin2.Span;
+import zipkin2.codec.SpanBytesEncoder;
+import zipkin2.reporter.Sender;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+final class ZipkinExporterHandler extends SpanExporter.Handler {
+ private static final Tracer tracer = Tracing.getTracer();
+ private static final Sampler probabilitySampler = Samplers.probabilitySampler(0.0001);
+ private static final Logger logger = Logger.getLogger(ZipkinExporterHandler.class.getName());
+
+ private static final String STATUS_CODE = "census.status_code";
+ private static final String STATUS_DESCRIPTION = "census.status_description";
+ private final SpanBytesEncoder encoder;
+ private final Sender sender;
+ private final Endpoint localEndpoint;
+
+ ZipkinExporterHandler(SpanBytesEncoder encoder, Sender sender, String serviceName) {
+ this.encoder = encoder;
+ this.sender = sender;
+ this.localEndpoint = produceLocalEndpoint(serviceName);
+ }
+
+ /** Logic borrowed from brave.internal.Platform.produceLocalEndpoint */
+ static Endpoint produceLocalEndpoint(String serviceName) {
+ Endpoint.Builder builder = Endpoint.newBuilder().serviceName(serviceName);
+ try {
+ Enumeration<NetworkInterface> nics = NetworkInterface.getNetworkInterfaces();
+ if (nics == null) {
+ return builder.build();
+ }
+ while (nics.hasMoreElements()) {
+ NetworkInterface nic = nics.nextElement();
+ Enumeration<InetAddress> addresses = nic.getInetAddresses();
+ while (addresses.hasMoreElements()) {
+ InetAddress address = addresses.nextElement();
+ if (address.isSiteLocalAddress()) {
+ builder.ip(address);
+ break;
+ }
+ }
+ }
+ } catch (Exception e) {
+ // don't crash the caller if there was a problem reading nics.
+ if (logger.isLoggable(Level.FINE)) {
+ logger.log(Level.FINE, "error reading nics", e);
+ }
+ }
+ return builder.build();
+ }
+
+ @SuppressWarnings("deprecation")
+ static Span generateSpan(SpanData spanData, Endpoint localEndpoint) {
+ SpanContext context = spanData.getContext();
+ long startTimestamp = toEpochMicros(spanData.getStartTimestamp());
+
+ // TODO(sebright): Fix the Checker Framework warning.
+ @SuppressWarnings("nullness")
+ long endTimestamp = toEpochMicros(spanData.getEndTimestamp());
+
+ // TODO(bdrutu): Fix the Checker Framework warning.
+ @SuppressWarnings("nullness")
+ Span.Builder spanBuilder =
+ Span.newBuilder()
+ .traceId(context.getTraceId().toLowerBase16())
+ .id(context.getSpanId().toLowerBase16())
+ .kind(toSpanKind(spanData))
+ .name(spanData.getName())
+ .timestamp(toEpochMicros(spanData.getStartTimestamp()))
+ .duration(endTimestamp - startTimestamp)
+ .localEndpoint(localEndpoint);
+
+ if (spanData.getParentSpanId() != null && spanData.getParentSpanId().isValid()) {
+ spanBuilder.parentId(spanData.getParentSpanId().toLowerBase16());
+ }
+
+ for (Map.Entry<String, AttributeValue> label :
+ spanData.getAttributes().getAttributeMap().entrySet()) {
+ spanBuilder.putTag(label.getKey(), attributeValueToString(label.getValue()));
+ }
+ Status status = spanData.getStatus();
+ if (status != null) {
+ spanBuilder.putTag(STATUS_CODE, status.getCanonicalCode().toString());
+ if (status.getDescription() != null) {
+ spanBuilder.putTag(STATUS_DESCRIPTION, status.getDescription());
+ }
+ }
+
+ for (TimedEvent<Annotation> annotation : spanData.getAnnotations().getEvents()) {
+ spanBuilder.addAnnotation(
+ toEpochMicros(annotation.getTimestamp()), annotation.getEvent().getDescription());
+ }
+
+ for (TimedEvent<io.opencensus.trace.MessageEvent> messageEvent :
+ spanData.getMessageEvents().getEvents()) {
+ spanBuilder.addAnnotation(
+ toEpochMicros(messageEvent.getTimestamp()), messageEvent.getEvent().getType().name());
+ }
+
+ return spanBuilder.build();
+ }
+
+ @javax.annotation.Nullable
+ private static Span.Kind toSpanKind(SpanData spanData) {
+ // This is a hack because the Span API did not have SpanKind.
+ if (spanData.getKind() == Kind.SERVER
+ || (spanData.getKind() == null && Boolean.TRUE.equals(spanData.getHasRemoteParent()))) {
+ return Span.Kind.SERVER;
+ }
+
+ // This is a hack because the Span API did not have SpanKind.
+ if (spanData.getKind() == Kind.CLIENT || spanData.getName().startsWith("Sent.")) {
+ return Span.Kind.CLIENT;
+ }
+
+ return null;
+ }
+
+ private static long toEpochMicros(Timestamp timestamp) {
+ return SECONDS.toMicros(timestamp.getSeconds()) + NANOSECONDS.toMicros(timestamp.getNanos());
+ }
+
+ // The return type needs to be nullable when this function is used as an argument to 'match' in
+ // attributeValueToString, because 'match' doesn't allow covariant return types.
+ private static final Function<Object, /*@Nullable*/ String> returnToString =
+ Functions.returnToString();
+
+ // TODO: Fix the Checker Framework warning.
+ @SuppressWarnings("nullness")
+ private static String attributeValueToString(AttributeValue attributeValue) {
+ return attributeValue.match(
+ returnToString,
+ returnToString,
+ returnToString,
+ returnToString,
+ Functions.<String>returnConstant(""));
+ }
+
+ @Override
+ public void export(Collection<SpanData> spanDataList) {
+ // Start a new span with explicit 1/10000 sampling probability to avoid the case when user
+ // sets the default sampler to always sample and we get the gRPC span of the zipkin
+ // export call always sampled and go to an infinite loop.
+ Scope scope =
+ tracer.spanBuilder("SendZipkinSpans").setSampler(probabilitySampler).startScopedSpan();
+ try {
+ List<byte[]> encodedSpans = new ArrayList<byte[]>(spanDataList.size());
+ for (SpanData spanData : spanDataList) {
+ encodedSpans.add(encoder.encode(generateSpan(spanData, localEndpoint)));
+ }
+ try {
+ sender.sendSpans(encodedSpans).execute();
+ } catch (IOException e) {
+ tracer
+ .getCurrentSpan()
+ .setStatus(
+ Status.UNKNOWN.withDescription(
+ e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
+ throw new RuntimeException(e); // TODO: should we instead do drop metrics?
+ }
+ } finally {
+ scope.close();
+ }
+ }
+}
diff --git a/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporter.java b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporter.java
new file mode 100644
index 00000000..aad5a563
--- /dev/null
+++ b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporter.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2017, 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.zipkin;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import zipkin2.Span;
+import zipkin2.codec.SpanBytesEncoder;
+import zipkin2.reporter.Sender;
+import zipkin2.reporter.urlconnection.URLConnectionSender;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Zipkin.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * ZipkinTraceExporter.createAndRegister("http://127.0.0.1:9411/api/v2/spans", "myservicename");
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @since 0.12
+ */
+public final class ZipkinTraceExporter {
+
+ private static final String REGISTER_NAME = ZipkinTraceExporter.class.getName();
+ private static final Object monitor = new Object();
+
+ @GuardedBy("monitor")
+ @Nullable
+ private static Handler handler = null;
+
+ private ZipkinTraceExporter() {}
+
+ /**
+ * Creates and registers the Zipkin Trace exporter to the OpenCensus library. Only one Zipkin
+ * exporter can be registered at any point.
+ *
+ * @param v2Url Ex http://127.0.0.1:9411/api/v2/spans
+ * @param serviceName the {@link Span#localServiceName() local service name} of the process.
+ * @throws IllegalStateException if a Zipkin exporter is already registered.
+ * @since 0.12
+ */
+ public static void createAndRegister(String v2Url, String serviceName) {
+ createAndRegister(SpanBytesEncoder.JSON_V2, URLConnectionSender.create(v2Url), serviceName);
+ }
+
+ /**
+ * Creates and registers the Zipkin Trace exporter to the OpenCensus library. Only one Zipkin
+ * exporter can be registered at any point.
+ *
+ * @param encoder Usually {@link SpanBytesEncoder#JSON_V2}
+ * @param sender Often, but not necessarily an http sender. This could be Kafka or SQS.
+ * @param serviceName the {@link Span#localServiceName() local service name} of the process.
+ * @throws IllegalStateException if a Zipkin exporter is already registered.
+ * @since 0.12
+ */
+ public static void createAndRegister(
+ SpanBytesEncoder encoder, Sender sender, String serviceName) {
+ synchronized (monitor) {
+ checkState(handler == null, "Zipkin exporter is already registered.");
+ Handler newHandler = new ZipkinExporterHandler(encoder, sender, serviceName);
+ handler = newHandler;
+ register(Tracing.getExportComponent().getSpanExporter(), newHandler);
+ }
+ }
+
+ /**
+ * Registers the {@code ZipkinTraceExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(SpanExporter spanExporter, Handler handler) {
+ spanExporter.registerHandler(REGISTER_NAME, handler);
+ }
+
+ /**
+ * Unregisters the Zipkin Trace exporter from the OpenCensus library.
+ *
+ * @throws IllegalStateException if a Zipkin exporter is not registered.
+ * @since 0.12
+ */
+ public static void unregister() {
+ synchronized (monitor) {
+ checkState(handler != null, "Zipkin exporter is not registered.");
+ unregister(Tracing.getExportComponent().getSpanExporter());
+ handler = null;
+ }
+ }
+
+ /**
+ * Unregisters the {@code ZipkinTraceExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(SpanExporter spanExporter) {
+ spanExporter.unregisterHandler(REGISTER_NAME);
+ }
+}
diff --git a/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandlerTest.java b/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandlerTest.java
new file mode 100644
index 00000000..7e293003
--- /dev/null
+++ b/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandlerTest.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2017, 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.zipkin;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+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.MessageEvent.Type;
+import io.opencensus.trace.Span.Kind;
+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 io.opencensus.trace.export.SpanData.Attributes;
+import io.opencensus.trace.export.SpanData.Links;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import zipkin2.Endpoint;
+import zipkin2.Span;
+
+/** Unit tests for {@link ZipkinExporterHandler}. */
+@RunWith(JUnit4.class)
+public class ZipkinExporterHandlerTest {
+ private static final Endpoint localEndpoint =
+ Endpoint.newBuilder().serviceName("tweetiebird").build();
+ private static final String TRACE_ID = "d239036e7d5cec116b562147388b35bf";
+ private static final String SPAN_ID = "9cc1e3049173be09";
+ private static final String PARENT_SPAN_ID = "8b03ab423da481c5";
+ private static final Map<String, AttributeValue> attributes = Collections.emptyMap();
+ private static final List<TimedEvent<Annotation>> annotations = Collections.emptyList();
+ private static final List<TimedEvent<MessageEvent>> messageEvents =
+ ImmutableList.of(
+ TimedEvent.create(
+ Timestamp.create(1505855799, 433901068),
+ MessageEvent.builder(Type.RECEIVED, 0).setCompressedMessageSize(7).build()),
+ TimedEvent.create(
+ Timestamp.create(1505855799, 459486280),
+ MessageEvent.builder(Type.SENT, 0).setCompressedMessageSize(13).build()));
+
+ @Test
+ public void generateSpan_NoKindAndRemoteParent() {
+ SpanData data =
+ SpanData.create(
+ SpanContext.create(
+ TraceId.fromLowerBase16(TRACE_ID),
+ SpanId.fromLowerBase16(SPAN_ID),
+ TraceOptions.builder().setIsSampled(true).build()),
+ // TODO SpanId.fromLowerBase16
+ SpanId.fromLowerBase16(PARENT_SPAN_ID),
+ true, /* hasRemoteParent */
+ "Recv.helloworld.Greeter.SayHello", /* name */
+ null, /* kind */
+ Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+ Attributes.create(attributes, 0 /* droppedAttributesCount */),
+ TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+ TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+ Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+ null, /* childSpanCount */
+ Status.OK,
+ Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+ assertThat(ZipkinExporterHandler.generateSpan(data, localEndpoint))
+ .isEqualTo(
+ Span.newBuilder()
+ .traceId(TRACE_ID)
+ .parentId(PARENT_SPAN_ID)
+ .id(SPAN_ID)
+ .kind(Span.Kind.SERVER)
+ .name(data.getName())
+ .timestamp(1505855794000000L + 194009601L / 1000)
+ .duration(
+ (1505855799000000L + 465726528L / 1000)
+ - (1505855794000000L + 194009601L / 1000))
+ .localEndpoint(localEndpoint)
+ .addAnnotation(1505855799000000L + 433901068L / 1000, "RECEIVED")
+ .addAnnotation(1505855799000000L + 459486280L / 1000, "SENT")
+ .putTag("census.status_code", "OK")
+ .build());
+ }
+
+ @Test
+ public void generateSpan_ServerKind() {
+ SpanData data =
+ SpanData.create(
+ SpanContext.create(
+ TraceId.fromLowerBase16(TRACE_ID),
+ SpanId.fromLowerBase16(SPAN_ID),
+ TraceOptions.builder().setIsSampled(true).build()),
+ // TODO SpanId.fromLowerBase16
+ SpanId.fromLowerBase16(PARENT_SPAN_ID),
+ true, /* hasRemoteParent */
+ "Recv.helloworld.Greeter.SayHello", /* name */
+ Kind.SERVER, /* kind */
+ Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+ Attributes.create(attributes, 0 /* droppedAttributesCount */),
+ TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+ TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+ Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+ null, /* childSpanCount */
+ Status.OK,
+ Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+ assertThat(ZipkinExporterHandler.generateSpan(data, localEndpoint))
+ .isEqualTo(
+ Span.newBuilder()
+ .traceId(TRACE_ID)
+ .parentId(PARENT_SPAN_ID)
+ .id(SPAN_ID)
+ .kind(Span.Kind.SERVER)
+ .name(data.getName())
+ .timestamp(1505855794000000L + 194009601L / 1000)
+ .duration(
+ (1505855799000000L + 465726528L / 1000)
+ - (1505855794000000L + 194009601L / 1000))
+ .localEndpoint(localEndpoint)
+ .addAnnotation(1505855799000000L + 433901068L / 1000, "RECEIVED")
+ .addAnnotation(1505855799000000L + 459486280L / 1000, "SENT")
+ .putTag("census.status_code", "OK")
+ .build());
+ }
+
+ @Test
+ public void generateSpan_ClientKind() {
+ SpanData data =
+ SpanData.create(
+ SpanContext.create(
+ TraceId.fromLowerBase16(TRACE_ID),
+ SpanId.fromLowerBase16(SPAN_ID),
+ TraceOptions.builder().setIsSampled(true).build()),
+ // TODO SpanId.fromLowerBase16
+ SpanId.fromLowerBase16(PARENT_SPAN_ID),
+ true, /* hasRemoteParent */
+ "Sent.helloworld.Greeter.SayHello", /* name */
+ Kind.CLIENT, /* kind */
+ Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+ Attributes.create(attributes, 0 /* droppedAttributesCount */),
+ TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+ TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+ Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+ null, /* childSpanCount */
+ Status.OK,
+ Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+ assertThat(ZipkinExporterHandler.generateSpan(data, localEndpoint))
+ .isEqualTo(
+ Span.newBuilder()
+ .traceId(TRACE_ID)
+ .parentId(PARENT_SPAN_ID)
+ .id(SPAN_ID)
+ .kind(Span.Kind.CLIENT)
+ .name(data.getName())
+ .timestamp(1505855794000000L + 194009601L / 1000)
+ .duration(
+ (1505855799000000L + 465726528L / 1000)
+ - (1505855794000000L + 194009601L / 1000))
+ .localEndpoint(localEndpoint)
+ .addAnnotation(1505855799000000L + 433901068L / 1000, "RECEIVED")
+ .addAnnotation(1505855799000000L + 459486280L / 1000, "SENT")
+ .putTag("census.status_code", "OK")
+ .build());
+ }
+
+ @Test
+ public void generateSpan_WithAttributes() {
+ Map<String, AttributeValue> attributeMap = new HashMap<String, AttributeValue>();
+ attributeMap.put("string", AttributeValue.stringAttributeValue("string value"));
+ attributeMap.put("boolean", AttributeValue.booleanAttributeValue(false));
+ attributeMap.put("long", AttributeValue.longAttributeValue(9999L));
+ SpanData data =
+ SpanData.create(
+ SpanContext.create(
+ TraceId.fromLowerBase16(TRACE_ID),
+ SpanId.fromLowerBase16(SPAN_ID),
+ TraceOptions.builder().setIsSampled(true).build()),
+ // TODO SpanId.fromLowerBase16
+ SpanId.fromLowerBase16(PARENT_SPAN_ID),
+ true, /* hasRemoteParent */
+ "Sent.helloworld.Greeter.SayHello", /* name */
+ Kind.CLIENT, /* kind */
+ Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+ Attributes.create(attributeMap, 0 /* droppedAttributesCount */),
+ TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+ TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+ Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+ null, /* childSpanCount */
+ Status.OK,
+ Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+ assertThat(ZipkinExporterHandler.generateSpan(data, localEndpoint))
+ .isEqualTo(
+ Span.newBuilder()
+ .traceId(TRACE_ID)
+ .parentId(PARENT_SPAN_ID)
+ .id(SPAN_ID)
+ .kind(Span.Kind.CLIENT)
+ .name(data.getName())
+ .timestamp(1505855794000000L + 194009601L / 1000)
+ .duration(
+ (1505855799000000L + 465726528L / 1000)
+ - (1505855794000000L + 194009601L / 1000))
+ .localEndpoint(localEndpoint)
+ .addAnnotation(1505855799000000L + 433901068L / 1000, "RECEIVED")
+ .addAnnotation(1505855799000000L + 459486280L / 1000, "SENT")
+ .putTag("census.status_code", "OK")
+ .putTag("string", "string value")
+ .putTag("boolean", "false")
+ .putTag("long", "9999")
+ .build());
+ }
+}
diff --git a/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporterTest.java b/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporterTest.java
new file mode 100644
index 00000000..2a032d0f
--- /dev/null
+++ b/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporterTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017, 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.zipkin;
+
+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 io.opencensus.trace.export.SpanExporter.Handler;
+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;
+
+/** Unit tests for {@link ZipkinTraceExporter}. */
+@RunWith(JUnit4.class)
+public class ZipkinTraceExporterTest {
+ @Mock private SpanExporter spanExporter;
+ @Mock private Handler handler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void registerUnregisterZipkinExporter() {
+ ZipkinTraceExporter.register(spanExporter, handler);
+ verify(spanExporter)
+ .registerHandler(
+ eq("io.opencensus.exporter.trace.zipkin.ZipkinTraceExporter"), same(handler));
+ ZipkinTraceExporter.unregister(spanExporter);
+ verify(spanExporter)
+ .unregisterHandler(eq("io.opencensus.exporter.trace.zipkin.ZipkinTraceExporter"));
+ }
+}