aboutsummaryrefslogtreecommitdiff
path: root/exporters
diff options
context:
space:
mode:
authorJulien Desprez <jdesprez@google.com>2018-10-22 11:37:22 -0700
committerandroid-build-merger <android-build-merger@google.com>2018-10-22 11:37:22 -0700
commit13217871fefa43f6d16fbb31b04e9904996d87d5 (patch)
treeede84fcf0a9687d4907ae5f8a4788271d62e0922 /exporters
parentcfbefd32336596ea63784607e4106dc37ce0567f (diff)
parent6fbc3cf5a1a3369fd354c1e5d9f90c86e4bce0a4 (diff)
downloadopencensus-java-13217871fefa43f6d16fbb31b04e9904996d87d5.tar.gz
Merge remote-tracking branch 'aosp/upstream-master' into merge am: dd3cabeacc
am: 6fbc3cf5a1 Change-Id: I11b0ec1cf561d2a14da78e444b1594f167787fe6
Diffstat (limited to 'exporters')
-rw-r--r--exporters/stats/prometheus/README.md81
-rw-r--r--exporters/stats/prometheus/build.gradle19
-rw-r--r--exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtils.java298
-rw-r--r--exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollector.java177
-rw-r--r--exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsConfiguration.java81
-rw-r--r--exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtilsTest.java326
-rw-r--r--exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollectorTest.java168
-rw-r--r--exporters/stats/signalfx/README.md76
-rw-r--r--exporters/stats/signalfx/build.gradle23
-rw-r--r--exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxMetricsSenderFactory.java59
-rw-r--r--exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptor.java188
-rw-r--r--exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfiguration.java153
-rw-r--r--exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporter.java109
-rw-r--r--exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThread.java105
-rw-r--r--exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptorTest.java320
-rw-r--r--exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfigurationTest.java90
-rw-r--r--exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterTest.java93
-rw-r--r--exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThreadTest.java149
-rw-r--r--exporters/stats/stackdriver/README.md171
-rw-r--r--exporters/stats/stackdriver/build.gradle30
-rw-r--r--exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtils.java518
-rw-r--r--exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorker.java274
-rw-r--r--exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfiguration.java159
-rw-r--r--exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporter.java363
-rw-r--r--exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtilsTest.java568
-rw-r--r--exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorkerTest.java310
-rw-r--r--exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfigurationTest.java72
-rw-r--r--exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporterTest.java129
-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
77 files changed, 11563 insertions, 0 deletions
diff --git a/exporters/stats/prometheus/README.md b/exporters/stats/prometheus/README.md
new file mode 100644
index 00000000..fa19efc9
--- /dev/null
+++ b/exporters/stats/prometheus/README.md
@@ -0,0 +1,81 @@
+# OpenCensus Prometheus Stats Exporter
+
+The *OpenCensus Prometheus Stats Exporter* is a stats exporter that exports data to
+Prometheus. [Prometheus](https://prometheus.io/) is an open-source systems monitoring and alerting
+toolkit originally built at [SoundCloud](https://soundcloud.com/).
+
+## Quickstart
+
+### Prerequisites
+
+To use this exporter, you need to install, configure and start Prometheus first. Follow the
+instructions [here](https://prometheus.io/docs/introduction/first_steps/).
+
+### Hello "Prometheus Stats"
+
+#### 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-stats-prometheus</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-stats-prometheus:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### Register the exporter
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) {
+ // Creates a PrometheusStatsCollector and registers it to the default Prometheus registry.
+ PrometheusStatsCollector.createAndRegister();
+
+ // Uses a simple Prometheus HTTPServer to export metrics.
+ // You can use a Prometheus PushGateway instead, though that's discouraged by Prometheus:
+ // https://prometheus.io/docs/practices/pushing/#should-i-be-using-the-pushgateway.
+ io.prometheus.client.exporter.HTTPServer server =
+ new HTTPServer(/*host*/ "localhost", /*port*/ 9091, /*daemon*/ true);
+
+ // Your code here.
+ // ...
+ }
+}
+```
+
+In this example, you should be able to see all the OpenCensus Prometheus metrics by visiting
+localhost:9091/metrics. Every time when you visit localhost:9091/metrics, the metrics will be
+collected from OpenCensus library and refreshed.
+
+#### Exporting
+
+After collecting stats from OpenCensus, there are multiple options for exporting them.
+See [Exporting via HTTP](https://github.com/prometheus/client_java#http), [Exporting to a Pushgateway](https://github.com/prometheus/client_java#exporting-to-a-pushgateway)
+and [Bridges](https://github.com/prometheus/client_java#bridges).
+
+#### Java Versions
+
+Java 7 or above is required for using this exporter.
+
+## FAQ
diff --git a/exporters/stats/prometheus/build.gradle b/exporters/stats/prometheus/build.gradle
new file mode 100644
index 00000000..fe8563c4
--- /dev/null
+++ b/exporters/stats/prometheus/build.gradle
@@ -0,0 +1,19 @@
+description = 'OpenCensus Stats Prometheus Exporter'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.7
+ it.targetCompatibility = 1.7
+}
+
+dependencies {
+ compileOnly libraries.auto_value
+
+ compile project(':opencensus-api'),
+ libraries.guava,
+ libraries.prometheus_simpleclient
+
+ 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/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtils.java b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtils.java
new file mode 100644
index 00000000..288813d3
--- /dev/null
+++ b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtils.java
@@ -0,0 +1,298 @@
+/*
+ * 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.stats.prometheus;
+
+import static io.prometheus.client.Collector.doubleToGoString;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.prometheus.client.Collector;
+import io.prometheus.client.Collector.MetricFamilySamples;
+import io.prometheus.client.Collector.MetricFamilySamples.Sample;
+import io.prometheus.client.Collector.Type;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map.Entry;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Util methods to convert OpenCensus Stats data models to Prometheus data models.
+ *
+ * <p>Each OpenCensus {@link View} will be converted to a Prometheus {@link MetricFamilySamples}
+ * with no {@link Sample}s, and is used for registering Prometheus {@code Metric}s. Only {@code
+ * Cumulative} views are supported. All views are under namespace "opencensus".
+ *
+ * <p>{@link Aggregation} will be converted to a corresponding Prometheus {@link Type}. {@link Sum}
+ * will be {@link Type#UNTYPED}, {@link Count} will be {@link Type#COUNTER}, {@link
+ * Aggregation.Mean} will be {@link Type#SUMMARY}, {@link Aggregation.LastValue} will be {@link
+ * Type#GAUGE} and {@link Distribution} will be {@link Type#HISTOGRAM}. Please note we cannot set
+ * bucket boundaries for custom {@link Type#HISTOGRAM}.
+ *
+ * <p>Each OpenCensus {@link ViewData} will be converted to a Prometheus {@link
+ * MetricFamilySamples}, and each {@code Row} of the {@link ViewData} will be converted to
+ * Prometheus {@link Sample}s.
+ *
+ * <p>{@link SumDataDouble}, {@link SumDataLong}, {@link LastValueDataDouble}, {@link
+ * LastValueDataLong} and {@link CountData} will be converted to a single {@link Sample}. {@link
+ * AggregationData.MeanData} will be converted to two {@link Sample}s sum and count. {@link
+ * DistributionData} will be converted to a list of {@link Sample}s that have the sum, count and
+ * histogram buckets.
+ *
+ * <p>{@link TagKey} and {@link TagValue} will be converted to Prometheus {@code LabelName} and
+ * {@code LabelValue}. {@code Null} {@link TagValue} will be converted to an empty string.
+ *
+ * <p>Please note that Prometheus Metric and Label name can only have alphanumeric characters and
+ * underscore. All other characters will be sanitized by underscores.
+ */
+@SuppressWarnings("deprecation")
+final class PrometheusExportUtils {
+
+ @VisibleForTesting static final String SAMPLE_SUFFIX_BUCKET = "_bucket";
+ @VisibleForTesting static final String SAMPLE_SUFFIX_COUNT = "_count";
+ @VisibleForTesting static final String SAMPLE_SUFFIX_SUM = "_sum";
+ @VisibleForTesting static final String LABEL_NAME_BUCKET_BOUND = "le";
+
+ private static final Function<Object, Type> TYPE_UNTYPED_FUNCTION =
+ Functions.returnConstant(Type.UNTYPED);
+ private static final Function<Object, Type> TYPE_COUNTER_FUNCTION =
+ Functions.returnConstant(Type.COUNTER);
+ private static final Function<Object, Type> TYPE_HISTOGRAM_FUNCTION =
+ Functions.returnConstant(Type.HISTOGRAM);
+ private static final Function<Object, Type> TYPE_GAUGE_FUNCTION =
+ Functions.returnConstant(Type.GAUGE);
+
+ // Converts a ViewData to a Prometheus MetricFamilySamples.
+ static MetricFamilySamples createMetricFamilySamples(ViewData viewData) {
+ View view = viewData.getView();
+ String name = Collector.sanitizeMetricName(view.getName().asString());
+ Type type = getType(view.getAggregation(), view.getWindow());
+ List<String> labelNames = convertToLabelNames(view.getColumns());
+ List<Sample> samples = Lists.newArrayList();
+ for (Entry<List</*@Nullable*/ TagValue>, AggregationData> entry :
+ viewData.getAggregationMap().entrySet()) {
+ samples.addAll(
+ getSamples(name, labelNames, entry.getKey(), entry.getValue(), view.getAggregation()));
+ }
+ return new MetricFamilySamples(name, type, view.getDescription(), samples);
+ }
+
+ // Converts a View to a Prometheus MetricFamilySamples.
+ // Used only for Prometheus metric registry, should not contain any actual samples.
+ static MetricFamilySamples createDescribableMetricFamilySamples(View view) {
+ String name = Collector.sanitizeMetricName(view.getName().asString());
+ Type type = getType(view.getAggregation(), view.getWindow());
+ List<String> labelNames = convertToLabelNames(view.getColumns());
+ if (containsDisallowedLeLabelForHistogram(labelNames, type)) {
+ throw new IllegalStateException(
+ "Prometheus Histogram cannot have a label named 'le', "
+ + "because it is a reserved label for bucket boundaries. "
+ + "Please remove this tag key from your view.");
+ }
+ return new MetricFamilySamples(
+ name, type, view.getDescription(), Collections.<Sample>emptyList());
+ }
+
+ @VisibleForTesting
+ static Type getType(Aggregation aggregation, View.AggregationWindow window) {
+ if (!(window instanceof View.AggregationWindow.Cumulative)) {
+ return Type.UNTYPED;
+ }
+ return aggregation.match(
+ TYPE_UNTYPED_FUNCTION, // SUM
+ TYPE_COUNTER_FUNCTION, // COUNT
+ TYPE_HISTOGRAM_FUNCTION, // DISTRIBUTION
+ TYPE_GAUGE_FUNCTION, // LAST VALUE
+ new Function<Aggregation, Type>() {
+ @Override
+ public Type apply(Aggregation arg) {
+ if (arg instanceof Aggregation.Mean) {
+ return Type.SUMMARY;
+ }
+ return Type.UNTYPED;
+ }
+ });
+ }
+
+ // Converts a row in ViewData (a.k.a Entry<List<TagValue>, AggregationData>) to a list of
+ // Prometheus Samples.
+ @VisibleForTesting
+ static List<Sample> getSamples(
+ final String name,
+ final List<String> labelNames,
+ List</*@Nullable*/ TagValue> tagValues,
+ AggregationData aggregationData,
+ final Aggregation aggregation) {
+ Preconditions.checkArgument(
+ labelNames.size() == tagValues.size(), "Label names and tag values have different sizes.");
+ final List<Sample> samples = Lists.newArrayList();
+ final List<String> labelValues = new ArrayList<String>(tagValues.size());
+ for (TagValue tagValue : tagValues) {
+ String labelValue = tagValue == null ? "" : tagValue.asString();
+ labelValues.add(labelValue);
+ }
+
+ aggregationData.match(
+ new Function<SumDataDouble, Void>() {
+ @Override
+ public Void apply(SumDataDouble arg) {
+ samples.add(new Sample(name, labelNames, labelValues, arg.getSum()));
+ return null;
+ }
+ },
+ new Function<SumDataLong, Void>() {
+ @Override
+ public Void apply(SumDataLong arg) {
+ samples.add(new Sample(name, labelNames, labelValues, arg.getSum()));
+ return null;
+ }
+ },
+ new Function<CountData, Void>() {
+ @Override
+ public Void apply(CountData arg) {
+ samples.add(new Sample(name, labelNames, labelValues, arg.getCount()));
+ return null;
+ }
+ },
+ new Function<DistributionData, Void>() {
+ @Override
+ public Void apply(DistributionData arg) {
+ // For histogram buckets, manually add the bucket boundaries as "le" labels. See
+ // https://github.com/prometheus/client_java/commit/ed184d8e50c82e98bb2706723fff764424840c3a#diff-c505abbde72dd6bf36e89917b3469404R241
+ @SuppressWarnings("unchecked")
+ Distribution distribution = (Distribution) aggregation;
+ List<Double> boundaries = distribution.getBucketBoundaries().getBoundaries();
+ List<String> labelNamesWithLe = new ArrayList<String>(labelNames);
+ labelNamesWithLe.add(LABEL_NAME_BUCKET_BOUND);
+ long cumulativeCount = 0;
+ for (int i = 0; i < arg.getBucketCounts().size(); i++) {
+ List<String> labelValuesWithLe = new ArrayList<String>(labelValues);
+ // The label value of "le" is the upper inclusive bound.
+ // For the last bucket, it should be "+Inf".
+ String bucketBoundary =
+ doubleToGoString(
+ i < boundaries.size() ? boundaries.get(i) : Double.POSITIVE_INFINITY);
+ labelValuesWithLe.add(bucketBoundary);
+ cumulativeCount += arg.getBucketCounts().get(i);
+ samples.add(
+ new MetricFamilySamples.Sample(
+ name + SAMPLE_SUFFIX_BUCKET,
+ labelNamesWithLe,
+ labelValuesWithLe,
+ cumulativeCount));
+ }
+
+ samples.add(
+ new MetricFamilySamples.Sample(
+ name + SAMPLE_SUFFIX_COUNT, labelNames, labelValues, arg.getCount()));
+ samples.add(
+ new MetricFamilySamples.Sample(
+ name + SAMPLE_SUFFIX_SUM,
+ labelNames,
+ labelValues,
+ arg.getCount() * arg.getMean()));
+ return null;
+ }
+ },
+ new Function<LastValueDataDouble, Void>() {
+ @Override
+ public Void apply(LastValueDataDouble arg) {
+ samples.add(new Sample(name, labelNames, labelValues, arg.getLastValue()));
+ return null;
+ }
+ },
+ new Function<LastValueDataLong, Void>() {
+ @Override
+ public Void apply(LastValueDataLong arg) {
+ samples.add(new Sample(name, labelNames, labelValues, arg.getLastValue()));
+ return null;
+ }
+ },
+ new Function<AggregationData, Void>() {
+ @Override
+ public Void apply(AggregationData arg) {
+ // TODO(songya): remove this once Mean aggregation is completely removed. Before that
+ // we need to continue supporting Mean, since it could still be used by users and some
+ // deprecated RPC views.
+ if (arg instanceof AggregationData.MeanData) {
+ AggregationData.MeanData meanData = (AggregationData.MeanData) arg;
+ samples.add(
+ new MetricFamilySamples.Sample(
+ name + SAMPLE_SUFFIX_COUNT, labelNames, labelValues, meanData.getCount()));
+ samples.add(
+ new MetricFamilySamples.Sample(
+ name + SAMPLE_SUFFIX_SUM,
+ labelNames,
+ labelValues,
+ meanData.getCount() * meanData.getMean()));
+ return null;
+ }
+ throw new IllegalArgumentException("Unknown Aggregation.");
+ }
+ });
+
+ return samples;
+ }
+
+ // Converts the list of tag keys to a list of string label names. Also sanitizes the tag keys.
+ @VisibleForTesting
+ static List<String> convertToLabelNames(List<TagKey> tagKeys) {
+ final List<String> labelNames = new ArrayList<String>(tagKeys.size());
+ for (TagKey tagKey : tagKeys) {
+ labelNames.add(Collector.sanitizeMetricName(tagKey.getName()));
+ }
+ return labelNames;
+ }
+
+ // Returns true if there is an "le" label name in histogram label names, returns false otherwise.
+ // Similar check to
+ // https://github.com/prometheus/client_java/commit/ed184d8e50c82e98bb2706723fff764424840c3a#diff-c505abbde72dd6bf36e89917b3469404R78
+ static boolean containsDisallowedLeLabelForHistogram(List<String> labelNames, Type type) {
+ if (!Type.HISTOGRAM.equals(type)) {
+ return false;
+ }
+ for (String label : labelNames) {
+ if (LABEL_NAME_BUCKET_BOUND.equals(label)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private PrometheusExportUtils() {}
+}
diff --git a/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollector.java b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollector.java
new file mode 100644
index 00000000..d555c92b
--- /dev/null
+++ b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollector.java
@@ -0,0 +1,177 @@
+/*
+ * 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.stats.prometheus;
+
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.containsDisallowedLeLabelForHistogram;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.convertToLabelNames;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.getType;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import io.opencensus.common.Scope;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import io.prometheus.client.Collector;
+import io.prometheus.client.CollectorRegistry;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * OpenCensus Stats {@link Collector} for Prometheus.
+ *
+ * @since 0.12
+ */
+@SuppressWarnings("deprecation")
+public final class PrometheusStatsCollector extends Collector implements Collector.Describable {
+
+ private static final Logger logger = Logger.getLogger(PrometheusStatsCollector.class.getName());
+ private static final Tracer tracer = Tracing.getTracer();
+ private static final Sampler probabilitySampler = Samplers.probabilitySampler(0.0001);
+
+ private final ViewManager viewManager;
+
+ /**
+ * Creates a {@link PrometheusStatsCollector} and registers it to Prometheus {@link
+ * CollectorRegistry#defaultRegistry}.
+ *
+ * <p>This is equivalent with:
+ *
+ * <pre>{@code
+ * PrometheusStatsCollector.createAndRegister(PrometheusStatsConfiguration.builder().build());
+ * }</pre>
+ *
+ * @throws IllegalArgumentException if a {@code PrometheusStatsCollector} has already been created
+ * and registered.
+ * @since 0.12
+ */
+ public static void createAndRegister() {
+ new PrometheusStatsCollector(Stats.getViewManager()).register();
+ }
+
+ /**
+ * Creates a {@link PrometheusStatsCollector} and registers it to the given Prometheus {@link
+ * CollectorRegistry} in the {@link PrometheusStatsConfiguration}.
+ *
+ * <p>If {@code CollectorRegistry} of the configuration is not set, the collector will use {@link
+ * CollectorRegistry#defaultRegistry}.
+ *
+ * @throws IllegalArgumentException if a {@code PrometheusStatsCollector} has already been created
+ * and registered.
+ * @since 0.13
+ */
+ public static void createAndRegister(PrometheusStatsConfiguration configuration) {
+ CollectorRegistry registry = configuration.getRegistry();
+ if (registry == null) {
+ registry = CollectorRegistry.defaultRegistry;
+ }
+ new PrometheusStatsCollector(Stats.getViewManager()).register(registry);
+ }
+
+ @Override
+ public List<MetricFamilySamples> collect() {
+ List<MetricFamilySamples> samples = Lists.newArrayList();
+ Span span =
+ tracer
+ .spanBuilder("ExportStatsToPrometheus")
+ .setSampler(probabilitySampler)
+ .setRecordEvents(true)
+ .startSpan();
+ span.addAnnotation("Collect Prometheus Metric Samples.");
+ Scope scope = tracer.withSpan(span);
+ try {
+ for (View view : viewManager.getAllExportedViews()) {
+ if (containsDisallowedLeLabelForHistogram(
+ convertToLabelNames(view.getColumns()),
+ getType(view.getAggregation(), view.getWindow()))) {
+ continue; // silently skip Distribution views with "le" tag key
+ }
+ try {
+ ViewData viewData = viewManager.getView(view.getName());
+ if (viewData == null) {
+ continue;
+ } else {
+ samples.add(PrometheusExportUtils.createMetricFamilySamples(viewData));
+ }
+ } catch (Throwable e) {
+ logger.log(Level.WARNING, "Exception thrown when collecting metric samples.", e);
+ span.setStatus(
+ Status.UNKNOWN.withDescription(
+ "Exception thrown when collecting Prometheus Metric Samples: "
+ + exceptionMessage(e)));
+ }
+ }
+ span.addAnnotation("Finish collecting Prometheus Metric Samples.");
+ } finally {
+ scope.close();
+ span.end();
+ }
+ return samples;
+ }
+
+ @Override
+ public List<MetricFamilySamples> describe() {
+ List<MetricFamilySamples> samples = Lists.newArrayList();
+ Span span =
+ tracer
+ .spanBuilder("DescribeMetricsForPrometheus")
+ .setSampler(probabilitySampler)
+ .setRecordEvents(true)
+ .startSpan();
+ span.addAnnotation("Describe Prometheus Metrics.");
+ Scope scope = tracer.withSpan(span);
+ try {
+ for (View view : viewManager.getAllExportedViews()) {
+ try {
+ samples.add(PrometheusExportUtils.createDescribableMetricFamilySamples(view));
+ } catch (Throwable e) {
+ logger.log(Level.WARNING, "Exception thrown when describing metrics.", e);
+ span.setStatus(
+ Status.UNKNOWN.withDescription(
+ "Exception thrown when describing Prometheus Metrics: " + exceptionMessage(e)));
+ }
+ }
+ span.addAnnotation("Finish describing Prometheus Metrics.");
+ } finally {
+ scope.close();
+ span.end();
+ }
+ return samples;
+ }
+
+ @VisibleForTesting
+ PrometheusStatsCollector(ViewManager viewManager) {
+ this.viewManager = viewManager;
+ Tracing.getExportComponent()
+ .getSampledSpanStore()
+ .registerSpanNamesForCollection(
+ ImmutableList.of("DescribeMetricsForPrometheus", "ExportStatsToPrometheus"));
+ }
+
+ private static String exceptionMessage(Throwable e) {
+ return e.getMessage() != null ? e.getMessage() : e.getClass().getName();
+ }
+}
diff --git a/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsConfiguration.java b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsConfiguration.java
new file mode 100644
index 00000000..3e8b95ed
--- /dev/null
+++ b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsConfiguration.java
@@ -0,0 +1,81 @@
+/*
+ * 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.stats.prometheus;
+
+import com.google.auto.value.AutoValue;
+import io.prometheus.client.CollectorRegistry;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Configurations for {@link PrometheusStatsCollector}.
+ *
+ * @since 0.13
+ */
+@AutoValue
+@Immutable
+public abstract class PrometheusStatsConfiguration {
+
+ PrometheusStatsConfiguration() {}
+
+ /**
+ * Returns the Prometheus {@link CollectorRegistry}.
+ *
+ * @return the Prometheus {@code CollectorRegistry}.
+ * @since 0.13
+ */
+ @Nullable
+ public abstract CollectorRegistry getRegistry();
+
+ /**
+ * Returns a new {@link Builder}.
+ *
+ * @return a {@code Builder}.
+ * @since 0.13
+ */
+ public static Builder builder() {
+ return new AutoValue_PrometheusStatsConfiguration.Builder();
+ }
+
+ /**
+ * Builder for {@link PrometheusStatsConfiguration}.
+ *
+ * @since 0.13
+ */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ Builder() {}
+
+ /**
+ * Sets the given Prometheus {@link CollectorRegistry}.
+ *
+ * @param registry the Prometheus {@code CollectorRegistry}.
+ * @return this.
+ * @since 0.13
+ */
+ public abstract Builder setRegistry(CollectorRegistry registry);
+
+ /**
+ * Builds a new {@link PrometheusStatsConfiguration} with current settings.
+ *
+ * @return a {@code PrometheusStatsConfiguration}.
+ * @since 0.13
+ */
+ public abstract PrometheusStatsConfiguration build();
+ }
+}
diff --git a/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtilsTest.java b/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtilsTest.java
new file mode 100644
index 00000000..ca8315b9
--- /dev/null
+++ b/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtilsTest.java
@@ -0,0 +1,326 @@
+/*
+ * 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.stats.prometheus;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.LABEL_NAME_BUCKET_BOUND;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.SAMPLE_SUFFIX_BUCKET;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.SAMPLE_SUFFIX_COUNT;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.SAMPLE_SUFFIX_SUM;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.convertToLabelNames;
+
+import com.google.common.collect.ImmutableMap;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewData.AggregationWindowData.IntervalData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.prometheus.client.Collector.MetricFamilySamples;
+import io.prometheus.client.Collector.MetricFamilySamples.Sample;
+import io.prometheus.client.Collector.Type;
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link PrometheusExportUtils}. */
+@RunWith(JUnit4.class)
+public class PrometheusExportUtilsTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ private static final Duration ONE_SECOND = Duration.create(1, 0);
+ private static final Cumulative CUMULATIVE = Cumulative.create();
+ private static final Interval INTERVAL = Interval.create(ONE_SECOND);
+ private static final Sum SUM = Sum.create();
+ private static final Count COUNT = Count.create();
+ private static final Mean MEAN = Mean.create();
+ private static final BucketBoundaries BUCKET_BOUNDARIES =
+ BucketBoundaries.create(Arrays.asList(-5.0, 0.0, 5.0));
+ private static final Distribution DISTRIBUTION = Distribution.create(BUCKET_BOUNDARIES);
+ private static final LastValue LAST_VALUE = LastValue.create();
+ private static final View.Name VIEW_NAME_1 = View.Name.create("view1");
+ private static final View.Name VIEW_NAME_2 = View.Name.create("view2");
+ private static final View.Name VIEW_NAME_3 = View.Name.create("view-3");
+ private static final View.Name VIEW_NAME_4 = View.Name.create("-view4");
+ private static final String DESCRIPTION = "View description";
+ private static final MeasureDouble MEASURE_DOUBLE =
+ MeasureDouble.create("measure", "description", "1");
+ private static final TagKey K1 = TagKey.create("k1");
+ private static final TagKey K2 = TagKey.create("k2");
+ private static final TagKey K3 = TagKey.create("k-3");
+ private static final TagKey TAG_KEY_LE = TagKey.create(LABEL_NAME_BUCKET_BOUND);
+ private static final TagValue V1 = TagValue.create("v1");
+ private static final TagValue V2 = TagValue.create("v2");
+ private static final TagValue V3 = TagValue.create("v-3");
+ private static final SumDataDouble SUM_DATA_DOUBLE = SumDataDouble.create(-5.5);
+ private static final SumDataLong SUM_DATA_LONG = SumDataLong.create(123456789);
+ private static final CountData COUNT_DATA = CountData.create(12345);
+ private static final MeanData MEAN_DATA = MeanData.create(3.4, 22);
+ private static final DistributionData DISTRIBUTION_DATA =
+ DistributionData.create(4.4, 5, -3.2, 15.7, 135.22, Arrays.asList(0L, 2L, 2L, 1L));
+ private static final LastValueDataDouble LAST_VALUE_DATA_DOUBLE = LastValueDataDouble.create(7.9);
+ private static final LastValueDataLong LAST_VALUE_DATA_LONG = LastValueDataLong.create(66666666);
+ private static final View VIEW1 =
+ View.create(
+ VIEW_NAME_1, DESCRIPTION, MEASURE_DOUBLE, COUNT, Arrays.asList(K1, K2), CUMULATIVE);
+ private static final View VIEW2 =
+ View.create(VIEW_NAME_2, DESCRIPTION, MEASURE_DOUBLE, MEAN, Arrays.asList(K3), CUMULATIVE);
+ private static final View VIEW3 =
+ View.create(
+ VIEW_NAME_3, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(K1), CUMULATIVE);
+ private static final View VIEW4 =
+ View.create(VIEW_NAME_4, DESCRIPTION, MEASURE_DOUBLE, COUNT, Arrays.asList(K1), INTERVAL);
+ private static final View DISTRIBUTION_VIEW_WITH_LE_KEY =
+ View.create(
+ VIEW_NAME_1,
+ DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(K1, TAG_KEY_LE),
+ CUMULATIVE);
+ private static final CumulativeData CUMULATIVE_DATA =
+ CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000));
+ private static final IntervalData INTERVAL_DATA = IntervalData.create(Timestamp.fromMillis(1000));
+ private static final String SAMPLE_NAME = "view";
+
+ @Test
+ public void testConstants() {
+ assertThat(SAMPLE_SUFFIX_BUCKET).isEqualTo("_bucket");
+ assertThat(SAMPLE_SUFFIX_COUNT).isEqualTo("_count");
+ assertThat(SAMPLE_SUFFIX_SUM).isEqualTo("_sum");
+ assertThat(LABEL_NAME_BUCKET_BOUND).isEqualTo("le");
+ }
+
+ @Test
+ public void getType() {
+ assertThat(PrometheusExportUtils.getType(COUNT, INTERVAL)).isEqualTo(Type.UNTYPED);
+ assertThat(PrometheusExportUtils.getType(COUNT, CUMULATIVE)).isEqualTo(Type.COUNTER);
+ assertThat(PrometheusExportUtils.getType(DISTRIBUTION, CUMULATIVE)).isEqualTo(Type.HISTOGRAM);
+ assertThat(PrometheusExportUtils.getType(SUM, CUMULATIVE)).isEqualTo(Type.UNTYPED);
+ assertThat(PrometheusExportUtils.getType(MEAN, CUMULATIVE)).isEqualTo(Type.SUMMARY);
+ assertThat(PrometheusExportUtils.getType(LAST_VALUE, CUMULATIVE)).isEqualTo(Type.GAUGE);
+ }
+
+ @Test
+ public void createDescribableMetricFamilySamples() {
+ assertThat(PrometheusExportUtils.createDescribableMetricFamilySamples(VIEW1))
+ .isEqualTo(
+ new MetricFamilySamples(
+ "view1", Type.COUNTER, DESCRIPTION, Collections.<Sample>emptyList()));
+ assertThat(PrometheusExportUtils.createDescribableMetricFamilySamples(VIEW2))
+ .isEqualTo(
+ new MetricFamilySamples(
+ "view2", Type.SUMMARY, DESCRIPTION, Collections.<Sample>emptyList()));
+ assertThat(PrometheusExportUtils.createDescribableMetricFamilySamples(VIEW3))
+ .isEqualTo(
+ new MetricFamilySamples(
+ "view_3", Type.HISTOGRAM, DESCRIPTION, Collections.<Sample>emptyList()));
+ assertThat(PrometheusExportUtils.createDescribableMetricFamilySamples(VIEW4))
+ .isEqualTo(
+ new MetricFamilySamples(
+ "_view4", Type.UNTYPED, DESCRIPTION, Collections.<Sample>emptyList()));
+ }
+
+ @Test
+ public void getSamples() {
+ assertThat(
+ PrometheusExportUtils.getSamples(
+ SAMPLE_NAME,
+ convertToLabelNames(Arrays.asList(K1, K2)),
+ Arrays.asList(V1, V2),
+ SUM_DATA_DOUBLE,
+ SUM))
+ .containsExactly(
+ new Sample(SAMPLE_NAME, Arrays.asList("k1", "k2"), Arrays.asList("v1", "v2"), -5.5));
+ assertThat(
+ PrometheusExportUtils.getSamples(
+ SAMPLE_NAME,
+ convertToLabelNames(Arrays.asList(K3)),
+ Arrays.asList(V3),
+ SUM_DATA_LONG,
+ SUM))
+ .containsExactly(
+ new Sample(SAMPLE_NAME, Arrays.asList("k_3"), Arrays.asList("v-3"), 123456789));
+ assertThat(
+ PrometheusExportUtils.getSamples(
+ SAMPLE_NAME,
+ convertToLabelNames(Arrays.asList(K1, K3)),
+ Arrays.asList(V1, null),
+ COUNT_DATA,
+ COUNT))
+ .containsExactly(
+ new Sample(SAMPLE_NAME, Arrays.asList("k1", "k_3"), Arrays.asList("v1", ""), 12345));
+ assertThat(
+ PrometheusExportUtils.getSamples(
+ SAMPLE_NAME,
+ convertToLabelNames(Arrays.asList(K3)),
+ Arrays.asList(V3),
+ MEAN_DATA,
+ MEAN))
+ .containsExactly(
+ new Sample(SAMPLE_NAME + "_count", Arrays.asList("k_3"), Arrays.asList("v-3"), 22),
+ new Sample(SAMPLE_NAME + "_sum", Arrays.asList("k_3"), Arrays.asList("v-3"), 74.8))
+ .inOrder();
+ assertThat(
+ PrometheusExportUtils.getSamples(
+ SAMPLE_NAME,
+ convertToLabelNames(Arrays.asList(K1)),
+ Arrays.asList(V1),
+ DISTRIBUTION_DATA,
+ DISTRIBUTION))
+ .containsExactly(
+ new Sample(
+ SAMPLE_NAME + "_bucket", Arrays.asList("k1", "le"), Arrays.asList("v1", "-5.0"), 0),
+ new Sample(
+ SAMPLE_NAME + "_bucket", Arrays.asList("k1", "le"), Arrays.asList("v1", "0.0"), 2),
+ new Sample(
+ SAMPLE_NAME + "_bucket", Arrays.asList("k1", "le"), Arrays.asList("v1", "5.0"), 4),
+ new Sample(
+ SAMPLE_NAME + "_bucket", Arrays.asList("k1", "le"), Arrays.asList("v1", "+Inf"), 5),
+ new Sample(SAMPLE_NAME + "_count", Arrays.asList("k1"), Arrays.asList("v1"), 5),
+ new Sample(SAMPLE_NAME + "_sum", Arrays.asList("k1"), Arrays.asList("v1"), 22.0))
+ .inOrder();
+ assertThat(
+ PrometheusExportUtils.getSamples(
+ SAMPLE_NAME,
+ convertToLabelNames(Arrays.asList(K1, K2)),
+ Arrays.asList(V1, V2),
+ LAST_VALUE_DATA_DOUBLE,
+ LAST_VALUE))
+ .containsExactly(
+ new Sample(SAMPLE_NAME, Arrays.asList("k1", "k2"), Arrays.asList("v1", "v2"), 7.9));
+ assertThat(
+ PrometheusExportUtils.getSamples(
+ SAMPLE_NAME,
+ convertToLabelNames(Arrays.asList(K3)),
+ Arrays.asList(V3),
+ LAST_VALUE_DATA_LONG,
+ LAST_VALUE))
+ .containsExactly(
+ new Sample(SAMPLE_NAME, Arrays.asList("k_3"), Arrays.asList("v-3"), 66666666));
+ }
+
+ @Test
+ public void getSamples_KeysAndValuesHaveDifferentSizes() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Label names and tag values have different sizes.");
+ PrometheusExportUtils.getSamples(
+ SAMPLE_NAME,
+ convertToLabelNames(Arrays.asList(K1, K2, K3)),
+ Arrays.asList(V1, V2),
+ DISTRIBUTION_DATA,
+ DISTRIBUTION);
+ }
+
+ @Test
+ public void createDescribableMetricFamilySamples_Histogram_DisallowLeLabelName() {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage(
+ "Prometheus Histogram cannot have a label named 'le', "
+ + "because it is a reserved label for bucket boundaries. "
+ + "Please remove this tag key from your view.");
+ PrometheusExportUtils.createDescribableMetricFamilySamples(DISTRIBUTION_VIEW_WITH_LE_KEY);
+ }
+
+ @Test
+ public void createMetricFamilySamples() {
+ assertThat(
+ PrometheusExportUtils.createMetricFamilySamples(
+ ViewData.create(
+ VIEW1, ImmutableMap.of(Arrays.asList(V1, V2), COUNT_DATA), CUMULATIVE_DATA)))
+ .isEqualTo(
+ new MetricFamilySamples(
+ "view1",
+ Type.COUNTER,
+ DESCRIPTION,
+ Arrays.asList(
+ new Sample(
+ "view1", Arrays.asList("k1", "k2"), Arrays.asList("v1", "v2"), 12345))));
+ assertThat(
+ PrometheusExportUtils.createMetricFamilySamples(
+ ViewData.create(
+ VIEW2, ImmutableMap.of(Arrays.asList(V1), MEAN_DATA), CUMULATIVE_DATA)))
+ .isEqualTo(
+ new MetricFamilySamples(
+ "view2",
+ Type.SUMMARY,
+ DESCRIPTION,
+ Arrays.asList(
+ new Sample("view2_count", Arrays.asList("k_3"), Arrays.asList("v1"), 22),
+ new Sample("view2_sum", Arrays.asList("k_3"), Arrays.asList("v1"), 74.8))));
+ assertThat(
+ PrometheusExportUtils.createMetricFamilySamples(
+ ViewData.create(
+ VIEW3, ImmutableMap.of(Arrays.asList(V3), DISTRIBUTION_DATA), CUMULATIVE_DATA)))
+ .isEqualTo(
+ new MetricFamilySamples(
+ "view_3",
+ Type.HISTOGRAM,
+ DESCRIPTION,
+ Arrays.asList(
+ new Sample(
+ "view_3_bucket",
+ Arrays.asList("k1", "le"),
+ Arrays.asList("v-3", "-5.0"),
+ 0),
+ new Sample(
+ "view_3_bucket", Arrays.asList("k1", "le"), Arrays.asList("v-3", "0.0"), 2),
+ new Sample(
+ "view_3_bucket", Arrays.asList("k1", "le"), Arrays.asList("v-3", "5.0"), 4),
+ new Sample(
+ "view_3_bucket",
+ Arrays.asList("k1", "le"),
+ Arrays.asList("v-3", "+Inf"),
+ 5),
+ new Sample("view_3_count", Arrays.asList("k1"), Arrays.asList("v-3"), 5),
+ new Sample("view_3_sum", Arrays.asList("k1"), Arrays.asList("v-3"), 22.0))));
+ assertThat(
+ PrometheusExportUtils.createMetricFamilySamples(
+ ViewData.create(
+ VIEW4, ImmutableMap.of(Arrays.asList(V1), COUNT_DATA), INTERVAL_DATA)))
+ .isEqualTo(
+ new MetricFamilySamples(
+ "_view4",
+ Type.UNTYPED,
+ DESCRIPTION,
+ Arrays.asList(
+ new Sample("_view4", Arrays.asList("k1"), Arrays.asList("v1"), 12345))));
+ }
+}
diff --git a/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollectorTest.java b/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollectorTest.java
new file mode 100644
index 00000000..3bd98451
--- /dev/null
+++ b/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollectorTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.stats.prometheus;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.LABEL_NAME_BUCKET_BOUND;
+import static org.mockito.Mockito.doReturn;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.prometheus.client.Collector.MetricFamilySamples;
+import io.prometheus.client.Collector.MetricFamilySamples.Sample;
+import io.prometheus.client.Collector.Type;
+import java.util.Arrays;
+import java.util.Collections;
+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 PrometheusStatsCollector}. */
+@RunWith(JUnit4.class)
+public class PrometheusStatsCollectorTest {
+
+ private static final Cumulative CUMULATIVE = Cumulative.create();
+ private static final BucketBoundaries BUCKET_BOUNDARIES =
+ BucketBoundaries.create(Arrays.asList(-5.0, 0.0, 5.0));
+ private static final Distribution DISTRIBUTION = Distribution.create(BUCKET_BOUNDARIES);
+ private static final View.Name VIEW_NAME = View.Name.create("view1");
+ private static final String DESCRIPTION = "View description";
+ private static final MeasureDouble MEASURE_DOUBLE =
+ MeasureDouble.create("measure", "description", "1");
+ private static final TagKey K1 = TagKey.create("k1");
+ private static final TagKey K2 = TagKey.create("k2");
+ private static final TagKey LE_TAG_KEY = TagKey.create(LABEL_NAME_BUCKET_BOUND);
+ private static final TagValue V1 = TagValue.create("v1");
+ private static final TagValue V2 = TagValue.create("v2");
+ private static final DistributionData DISTRIBUTION_DATA =
+ DistributionData.create(4.4, 5, -3.2, 15.7, 135.22, Arrays.asList(0L, 2L, 2L, 1L));
+ private static final View VIEW =
+ View.create(
+ VIEW_NAME, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(K1, K2), CUMULATIVE);
+ private static final View VIEW_WITH_LE_TAG_KEY =
+ View.create(
+ VIEW_NAME,
+ DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(K1, LE_TAG_KEY),
+ CUMULATIVE);
+ private static final CumulativeData CUMULATIVE_DATA =
+ CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000));
+ private static final ViewData VIEW_DATA =
+ ViewData.create(
+ VIEW, ImmutableMap.of(Arrays.asList(V1, V2), DISTRIBUTION_DATA), CUMULATIVE_DATA);
+ private static final ViewData VIEW_DATA_WITH_LE_TAG_KEY =
+ ViewData.create(
+ VIEW_WITH_LE_TAG_KEY,
+ ImmutableMap.of(Arrays.asList(V1, V2), DISTRIBUTION_DATA),
+ CUMULATIVE_DATA);
+
+ @Mock private ViewManager mockViewManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ doReturn(ImmutableSet.of(VIEW)).when(mockViewManager).getAllExportedViews();
+ doReturn(VIEW_DATA).when(mockViewManager).getView(VIEW_NAME);
+ }
+
+ @Test
+ public void testCollect() {
+ PrometheusStatsCollector collector = new PrometheusStatsCollector(mockViewManager);
+ String name = "view1";
+ assertThat(collector.collect())
+ .containsExactly(
+ new MetricFamilySamples(
+ "view1",
+ Type.HISTOGRAM,
+ "View description",
+ Arrays.asList(
+ new Sample(
+ name + "_bucket",
+ Arrays.asList("k1", "k2", "le"),
+ Arrays.asList("v1", "v2", "-5.0"),
+ 0),
+ new Sample(
+ name + "_bucket",
+ Arrays.asList("k1", "k2", "le"),
+ Arrays.asList("v1", "v2", "0.0"),
+ 2),
+ new Sample(
+ name + "_bucket",
+ Arrays.asList("k1", "k2", "le"),
+ Arrays.asList("v1", "v2", "5.0"),
+ 4),
+ new Sample(
+ name + "_bucket",
+ Arrays.asList("k1", "k2", "le"),
+ Arrays.asList("v1", "v2", "+Inf"),
+ 5),
+ new Sample(
+ name + "_count", Arrays.asList("k1", "k2"), Arrays.asList("v1", "v2"), 5),
+ new Sample(
+ name + "_sum",
+ Arrays.asList("k1", "k2"),
+ Arrays.asList("v1", "v2"),
+ 22.0))));
+ }
+
+ @Test
+ public void testCollect_SkipDistributionViewWithLeTagKey() {
+ doReturn(ImmutableSet.of(VIEW_WITH_LE_TAG_KEY)).when(mockViewManager).getAllExportedViews();
+ doReturn(VIEW_DATA_WITH_LE_TAG_KEY).when(mockViewManager).getView(VIEW_NAME);
+ PrometheusStatsCollector collector = new PrometheusStatsCollector(mockViewManager);
+ assertThat(collector.collect()).isEmpty();
+ }
+
+ @Test
+ public void testDescribe() {
+ PrometheusStatsCollector collector = new PrometheusStatsCollector(mockViewManager);
+ assertThat(collector.describe())
+ .containsExactly(
+ new MetricFamilySamples(
+ "view1", Type.HISTOGRAM, "View description", Collections.<Sample>emptyList()));
+ }
+
+ @Test
+ public void testCollect_WithNoopViewManager() {
+ PrometheusStatsCollector collector = new PrometheusStatsCollector(Stats.getViewManager());
+ assertThat(collector.collect()).isEmpty();
+ }
+
+ @Test
+ public void testDescribe_WithNoopViewManager() {
+ PrometheusStatsCollector collector = new PrometheusStatsCollector(Stats.getViewManager());
+ assertThat(collector.describe()).isEmpty();
+ }
+}
diff --git a/exporters/stats/signalfx/README.md b/exporters/stats/signalfx/README.md
new file mode 100644
index 00000000..7c61f896
--- /dev/null
+++ b/exporters/stats/signalfx/README.md
@@ -0,0 +1,76 @@
+# OpenCensus SignalFx Stats Exporter
+
+The _OpenCensus SignalFx Stats Exporter_ is a stats exporter that
+exports data to [SignalFx](https://signalfx.com), a real-time monitoring
+solution for cloud and distributed applications. SignalFx ingests that
+data and offers various visualizations on charts, dashboards and service
+maps, as well as real-time anomaly detection.
+
+## Quickstart
+
+### Prerequisites
+
+To use this exporter, you must have a [SignalFx](https://signalfx.com)
+account and corresponding [data ingest
+token](https://docs.signalfx.com/en/latest/admin-guide/tokens.html).
+
+#### Java versions
+
+This exporter requires Java 7 or above.
+
+### 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-stats-signalfx</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:
+
+```
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-stats-signalfx:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+### Register the exporter
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) {
+ // SignalFx token is read from Java system properties.
+ // Stats will be reported every second by default.
+ SignalFxStatsExporter.create(SignalFxStatsConfiguration.builder().build());
+ }
+}
+```
+
+If you want to pass in the token yourself, or set a different reporting
+interval, use:
+
+```java
+// Use token "your_signalfx_token" and report every 5 seconds.
+SignalFxStatsExporter.create(
+ SignalFxStatsConfiguration.builder()
+ .setToken("your_signalfx_token")
+ .setExportInterval(Duration.create(5, 0))
+ .build());
+```
diff --git a/exporters/stats/signalfx/build.gradle b/exporters/stats/signalfx/build.gradle
new file mode 100644
index 00000000..d496b1e5
--- /dev/null
+++ b/exporters/stats/signalfx/build.gradle
@@ -0,0 +1,23 @@
+description = 'OpenCensus SignalFx Stats Exporter'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.7
+ it.targetCompatibility = 1.7
+}
+
+dependencies {
+ compileOnly libraries.auto_value
+
+ compile project(':opencensus-api'),
+ libraries.guava
+
+ compile (libraries.signalfx_java) {
+ // Prefer library version.
+ exclude group: 'com.google.guava', module: '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/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxMetricsSenderFactory.java b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxMetricsSenderFactory.java
new file mode 100644
index 00000000..5601a54c
--- /dev/null
+++ b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxMetricsSenderFactory.java
@@ -0,0 +1,59 @@
+/*
+ * 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.stats.signalfx;
+
+import com.signalfx.endpoint.SignalFxEndpoint;
+import com.signalfx.metrics.auth.StaticAuthToken;
+import com.signalfx.metrics.connection.HttpDataPointProtobufReceiverFactory;
+import com.signalfx.metrics.connection.HttpEventProtobufReceiverFactory;
+import com.signalfx.metrics.errorhandler.OnSendErrorHandler;
+import com.signalfx.metrics.flush.AggregateMetricSender;
+import java.net.URI;
+import java.util.Collections;
+
+/** Interface for creators of {@link AggregateMetricSender}. */
+interface SignalFxMetricsSenderFactory {
+
+ /**
+ * Creates a new SignalFx metrics sender instance.
+ *
+ * @param endpoint The SignalFx ingest endpoint URL.
+ * @param token The SignalFx ingest token.
+ * @param errorHandler An {@link OnSendErrorHandler} through which errors when sending data to
+ * SignalFx will be communicated.
+ * @return The created {@link AggregateMetricSender} instance.
+ */
+ AggregateMetricSender create(URI endpoint, String token, OnSendErrorHandler errorHandler);
+
+ /** The default, concrete implementation of this interface. */
+ SignalFxMetricsSenderFactory DEFAULT =
+ new SignalFxMetricsSenderFactory() {
+ @Override
+ @SuppressWarnings("nullness")
+ public AggregateMetricSender create(
+ URI endpoint, String token, OnSendErrorHandler errorHandler) {
+ SignalFxEndpoint sfx =
+ new SignalFxEndpoint(endpoint.getScheme(), endpoint.getHost(), endpoint.getPort());
+ return new AggregateMetricSender(
+ null,
+ new HttpDataPointProtobufReceiverFactory(sfx).setVersion(2),
+ new HttpEventProtobufReceiverFactory(sfx),
+ new StaticAuthToken(token),
+ Collections.singleton(errorHandler));
+ }
+ };
+}
diff --git a/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptor.java b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptor.java
new file mode 100644
index 00000000..2eb75c4c
--- /dev/null
+++ b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptor.java
@@ -0,0 +1,188 @@
+/*
+ * 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.stats.signalfx;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.DataPoint;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Datum;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Dimension;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.MetricType;
+import io.opencensus.common.Function;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Adapter for a {@code ViewData}'s contents into SignalFx datapoints. */
+@SuppressWarnings("deprecation")
+final class SignalFxSessionAdaptor {
+
+ private SignalFxSessionAdaptor() {}
+
+ /**
+ * Converts the given view data into datapoints that can be sent to SignalFx.
+ *
+ * <p>The view name is used as the metric name, and the aggregation type and aggregation window
+ * type determine the metric type.
+ *
+ * @param data The {@link ViewData} containing the aggregation data of each combination of tag
+ * values.
+ * @return A list of datapoints for the corresponding metric timeseries of this view's metric.
+ */
+ static List<DataPoint> adapt(ViewData data) {
+ View view = data.getView();
+ List<TagKey> keys = view.getColumns();
+
+ MetricType metricType = getMetricTypeForAggregation(view.getAggregation(), view.getWindow());
+ if (metricType == null) {
+ return Collections.emptyList();
+ }
+
+ List<DataPoint> datapoints = new ArrayList<>(data.getAggregationMap().size());
+ for (Map.Entry<List</*@Nullable*/ TagValue>, AggregationData> entry :
+ data.getAggregationMap().entrySet()) {
+ datapoints.add(
+ DataPoint.newBuilder()
+ .setMetric(view.getName().asString())
+ .setMetricType(metricType)
+ .addAllDimensions(createDimensions(keys, entry.getKey()))
+ .setValue(createDatum(entry.getValue()))
+ .build());
+ }
+ return datapoints;
+ }
+
+ @VisibleForTesting
+ @javax.annotation.Nullable
+ static MetricType getMetricTypeForAggregation(
+ Aggregation aggregation, View.AggregationWindow window) {
+ if (aggregation instanceof Aggregation.Mean || aggregation instanceof Aggregation.LastValue) {
+ return MetricType.GAUGE;
+ } else if (aggregation instanceof Aggregation.Count || aggregation instanceof Aggregation.Sum) {
+ if (window instanceof View.AggregationWindow.Cumulative) {
+ return MetricType.CUMULATIVE_COUNTER;
+ }
+ // TODO(mpetazzoni): support incremental counters when AggregationWindow.Interval is ready
+ }
+
+ // TODO(mpetazzoni): add support for histograms (Aggregation.Distribution).
+ return null;
+ }
+
+ @VisibleForTesting
+ static Iterable<Dimension> createDimensions(
+ List<TagKey> keys, List</*@Nullable*/ TagValue> values) {
+ Preconditions.checkArgument(
+ keys.size() == values.size(), "TagKeys and TagValues don't have the same size.");
+ List<Dimension> dimensions = new ArrayList<>(keys.size());
+ for (ListIterator<TagKey> it = keys.listIterator(); it.hasNext(); ) {
+ TagKey key = it.next();
+ TagValue value = values.get(it.previousIndex());
+ if (value == null || Strings.isNullOrEmpty(value.asString())) {
+ continue;
+ }
+ dimensions.add(createDimension(key, value));
+ }
+ return dimensions;
+ }
+
+ @VisibleForTesting
+ static Dimension createDimension(TagKey key, TagValue value) {
+ return Dimension.newBuilder().setKey(key.getName()).setValue(value.asString()).build();
+ }
+
+ @VisibleForTesting
+ static Datum createDatum(AggregationData data) {
+ final Datum.Builder builder = Datum.newBuilder();
+ data.match(
+ new Function<SumDataDouble, Void>() {
+ @Override
+ public Void apply(SumDataDouble arg) {
+ builder.setDoubleValue(arg.getSum());
+ return null;
+ }
+ },
+ new Function<SumDataLong, Void>() {
+ @Override
+ public Void apply(SumDataLong arg) {
+ builder.setIntValue(arg.getSum());
+ return null;
+ }
+ },
+ new Function<CountData, Void>() {
+ @Override
+ public Void apply(CountData arg) {
+ builder.setIntValue(arg.getCount());
+ return null;
+ }
+ },
+ new Function<DistributionData, Void>() {
+ @Override
+ public Void apply(DistributionData arg) {
+ // TODO(mpetazzoni): add histogram support.
+ throw new IllegalArgumentException("Distribution aggregations are not supported");
+ }
+ },
+ new Function<LastValueDataDouble, Void>() {
+ @Override
+ public Void apply(LastValueDataDouble arg) {
+ builder.setDoubleValue(arg.getLastValue());
+ return null;
+ }
+ },
+ new Function<LastValueDataLong, Void>() {
+ @Override
+ public Void apply(LastValueDataLong arg) {
+ builder.setIntValue(arg.getLastValue());
+ return null;
+ }
+ },
+ new Function<AggregationData, Void>() {
+ @Override
+ public Void apply(AggregationData arg) {
+ // TODO(songya): remove this once Mean aggregation is completely removed. Before that
+ // we need to continue supporting Mean, since it could still be used by users and some
+ // deprecated RPC views.
+ if (arg instanceof AggregationData.MeanData) {
+ builder.setDoubleValue(((AggregationData.MeanData) arg).getMean());
+ return null;
+ }
+ throw new IllegalArgumentException("Unknown Aggregation.");
+ }
+ });
+ return builder.build();
+ }
+}
diff --git a/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfiguration.java b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfiguration.java
new file mode 100644
index 00000000..e8b4d756
--- /dev/null
+++ b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfiguration.java
@@ -0,0 +1,153 @@
+/*
+ * 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.stats.signalfx;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import io.opencensus.common.Duration;
+import java.net.URI;
+import java.net.URISyntaxException;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Configurations for {@link SignalFxStatsExporter}.
+ *
+ * @since 0.11
+ */
+@AutoValue
+@Immutable
+public abstract class SignalFxStatsConfiguration {
+
+ /**
+ * The default SignalFx ingest API URL.
+ *
+ * @since 0.11
+ */
+ public static final URI DEFAULT_SIGNALFX_ENDPOINT;
+
+ static {
+ try {
+ DEFAULT_SIGNALFX_ENDPOINT = new URI("https://ingest.signalfx.com");
+ } catch (URISyntaxException e) {
+ // This shouldn't happen if DEFAULT_SIGNALFX_ENDPOINT was typed in correctly.
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * The default stats export interval.
+ *
+ * @since 0.11
+ */
+ public static final Duration DEFAULT_EXPORT_INTERVAL = Duration.create(1, 0);
+
+ private static final Duration ZERO = Duration.create(0, 0);
+
+ SignalFxStatsConfiguration() {}
+
+ /**
+ * Returns the SignalFx ingest API URL.
+ *
+ * @return the SignalFx ingest API URL.
+ * @since 0.11
+ */
+ public abstract URI getIngestEndpoint();
+
+ /**
+ * Returns the authentication token.
+ *
+ * @return the authentication token.
+ * @since 0.11
+ */
+ public abstract String getToken();
+
+ /**
+ * Returns the export interval between pushes to SignalFx.
+ *
+ * @return the export interval.
+ * @since 0.11
+ */
+ public abstract Duration getExportInterval();
+
+ /**
+ * Returns a new {@link Builder}.
+ *
+ * @return a {@code Builder}.
+ * @since 0.11
+ */
+ public static Builder builder() {
+ return new AutoValue_SignalFxStatsConfiguration.Builder()
+ .setIngestEndpoint(DEFAULT_SIGNALFX_ENDPOINT)
+ .setExportInterval(DEFAULT_EXPORT_INTERVAL);
+ }
+
+ /**
+ * Builder for {@link SignalFxStatsConfiguration}.
+ *
+ * @since 0.11
+ */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ Builder() {}
+
+ /**
+ * Sets the given SignalFx ingest API URL.
+ *
+ * @param url the SignalFx ingest API URL.
+ * @return this.
+ * @since 0.11
+ */
+ public abstract Builder setIngestEndpoint(URI url);
+
+ /**
+ * Sets the given authentication token.
+ *
+ * @param token the authentication token.
+ * @return this.
+ * @since 0.11
+ */
+ public abstract Builder setToken(String token);
+
+ /**
+ * Sets the export interval.
+ *
+ * @param exportInterval the export interval between pushes to SignalFx.
+ * @return this.
+ * @since 0.11
+ */
+ public abstract Builder setExportInterval(Duration exportInterval);
+
+ abstract SignalFxStatsConfiguration autoBuild();
+
+ /**
+ * Builds a new {@link SignalFxStatsConfiguration} with current settings.
+ *
+ * @return a {@code SignalFxStatsConfiguration}.
+ * @since 0.11
+ */
+ public SignalFxStatsConfiguration build() {
+ SignalFxStatsConfiguration config = autoBuild();
+ Preconditions.checkArgument(
+ !Strings.isNullOrEmpty(config.getToken()), "Invalid SignalFx token");
+ Preconditions.checkArgument(
+ config.getExportInterval().compareTo(ZERO) > 0, "Interval duration must be positive");
+ return config;
+ }
+ }
+}
diff --git a/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporter.java b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporter.java
new file mode 100644
index 00000000..f7915b71
--- /dev/null
+++ b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporter.java
@@ -0,0 +1,109 @@
+/*
+ * 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.stats.signalfx;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.ViewManager;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Exporter to SignalFx.
+ *
+ * <p>Example of usage:
+ *
+ * <pre><code>
+ * public static void main(String[] args) {
+ * SignalFxStatsExporter.create(SignalFxStatsConfiguration.builder().build());
+ * ... // Do work.
+ * }
+ * </code></pre>
+ *
+ * @since 0.11
+ */
+public final class SignalFxStatsExporter {
+
+ private static final Object monitor = new Object();
+
+ private final SignalFxStatsConfiguration configuration;
+ private final SignalFxStatsExporterWorkerThread workerThread;
+
+ @GuardedBy("monitor")
+ @Nullable
+ private static SignalFxStatsExporter exporter = null;
+
+ private SignalFxStatsExporter(SignalFxStatsConfiguration configuration, ViewManager viewManager) {
+ Preconditions.checkNotNull(configuration, "SignalFx stats exporter configuration");
+ this.configuration = configuration;
+ this.workerThread =
+ new SignalFxStatsExporterWorkerThread(
+ SignalFxMetricsSenderFactory.DEFAULT,
+ configuration.getIngestEndpoint(),
+ configuration.getToken(),
+ configuration.getExportInterval(),
+ viewManager);
+ }
+
+ /**
+ * Creates a SignalFx Stats exporter from the given {@link SignalFxStatsConfiguration}.
+ *
+ * <p>If {@code ingestEndpoint} is not set on the configuration, the exporter will use {@link
+ * SignalFxStatsConfiguration#DEFAULT_SIGNALFX_ENDPOINT}.
+ *
+ * <p>If {@code exportInterval} is not set on the configuration, the exporter will use {@link
+ * SignalFxStatsConfiguration#DEFAULT_EXPORT_INTERVAL}.
+ *
+ * @param configuration the {@code SignalFxStatsConfiguration}.
+ * @throws IllegalStateException if a SignalFx exporter is already created.
+ * @since 0.11
+ */
+ public static void create(SignalFxStatsConfiguration configuration) {
+ synchronized (monitor) {
+ Preconditions.checkState(exporter == null, "SignalFx stats exporter is already created.");
+ exporter = new SignalFxStatsExporter(configuration, Stats.getViewManager());
+ exporter.workerThread.start();
+ }
+ }
+
+ @VisibleForTesting
+ static void unsafeResetExporter() {
+ synchronized (monitor) {
+ if (exporter != null) {
+ SignalFxStatsExporterWorkerThread workerThread = exporter.workerThread;
+ if (workerThread != null && workerThread.isAlive()) {
+ try {
+ workerThread.interrupt();
+ workerThread.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ exporter = null;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ @Nullable
+ static SignalFxStatsConfiguration unsafeGetConfig() {
+ synchronized (monitor) {
+ return exporter != null ? exporter.configuration : null;
+ }
+ }
+}
diff --git a/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThread.java b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThread.java
new file mode 100644
index 00000000..348778e2
--- /dev/null
+++ b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThread.java
@@ -0,0 +1,105 @@
+/*
+ * 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.stats.signalfx;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.signalfx.metrics.errorhandler.MetricError;
+import com.signalfx.metrics.errorhandler.OnSendErrorHandler;
+import com.signalfx.metrics.flush.AggregateMetricSender;
+import com.signalfx.metrics.flush.AggregateMetricSender.Session;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.DataPoint;
+import io.opencensus.common.Duration;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import java.io.IOException;
+import java.net.URI;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Worker {@code Thread} that polls ViewData from the Stats's ViewManager and exports to SignalFx.
+ *
+ * <p>{@code SignalFxStatsExporterWorkerThread} is a daemon {@code Thread}
+ */
+final class SignalFxStatsExporterWorkerThread extends Thread {
+
+ private static final Logger logger =
+ Logger.getLogger(SignalFxStatsExporterWorkerThread.class.getName());
+
+ private static final OnSendErrorHandler ERROR_HANDLER =
+ new OnSendErrorHandler() {
+ @Override
+ public void handleError(MetricError error) {
+ logger.log(Level.WARNING, "Unable to send metrics to SignalFx: {0}", error.getMessage());
+ }
+ };
+
+ private final long intervalMs;
+ private final ViewManager views;
+ private final AggregateMetricSender sender;
+
+ SignalFxStatsExporterWorkerThread(
+ SignalFxMetricsSenderFactory factory,
+ URI endpoint,
+ String token,
+ Duration interval,
+ ViewManager views) {
+ this.intervalMs = interval.toMillis();
+ this.views = views;
+ this.sender = factory.create(endpoint, token, ERROR_HANDLER);
+
+ setDaemon(true);
+ setName(getClass().getSimpleName());
+ logger.log(Level.FINE, "Initialized SignalFx exporter to {0}.", endpoint);
+ }
+
+ @VisibleForTesting
+ void export() throws IOException {
+ Session session = sender.createSession();
+ try {
+ for (View view : views.getAllExportedViews()) {
+ ViewData data = views.getView(view.getName());
+ if (data == null) {
+ continue;
+ }
+
+ for (DataPoint datapoint : SignalFxSessionAdaptor.adapt(data)) {
+ session.setDatapoint(datapoint);
+ }
+ }
+ } finally {
+ session.close();
+ }
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ export();
+ Thread.sleep(intervalMs);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ break;
+ } catch (Throwable e) {
+ logger.log(Level.WARNING, "Exception thrown by the SignalFx stats exporter", e);
+ }
+ }
+ logger.log(Level.INFO, "SignalFx stats exporter stopped.");
+ }
+}
diff --git a/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptorTest.java b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptorTest.java
new file mode 100644
index 00000000..34f4dfa7
--- /dev/null
+++ b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptorTest.java
@@ -0,0 +1,320 @@
+/*
+ * 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.stats.signalfx;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.DataPoint;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Datum;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Dimension;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.MetricType;
+import io.opencensus.common.Duration;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow;
+import io.opencensus.stats.View.Name;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SignalFxSessionAdaptorTest {
+
+ private static final Duration ONE_SECOND = Duration.create(1, 0);
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Mock private View view;
+
+ @Mock private ViewData viewData;
+
+ @Before
+ public void setUp() {
+ Mockito.when(view.getName()).thenReturn(Name.create("view-name"));
+ Mockito.when(view.getColumns()).thenReturn(ImmutableList.of(TagKey.create("animal")));
+ Mockito.when(viewData.getView()).thenReturn(view);
+ }
+
+ @Test
+ public void checkMetricTypeFromAggregation() {
+ assertNull(SignalFxSessionAdaptor.getMetricTypeForAggregation(null, null));
+ assertNull(
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ null, AggregationWindow.Cumulative.create()));
+ assertEquals(
+ MetricType.GAUGE,
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.Mean.create(), AggregationWindow.Cumulative.create()));
+ assertEquals(
+ MetricType.GAUGE,
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.Mean.create(), AggregationWindow.Interval.create(ONE_SECOND)));
+ assertEquals(
+ MetricType.CUMULATIVE_COUNTER,
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.Count.create(), AggregationWindow.Cumulative.create()));
+ assertEquals(
+ MetricType.CUMULATIVE_COUNTER,
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.Sum.create(), AggregationWindow.Cumulative.create()));
+ assertNull(
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(Aggregation.Count.create(), null));
+ assertNull(SignalFxSessionAdaptor.getMetricTypeForAggregation(Aggregation.Sum.create(), null));
+ assertNull(
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.Count.create(), AggregationWindow.Interval.create(ONE_SECOND)));
+ assertNull(
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.Sum.create(), AggregationWindow.Interval.create(ONE_SECOND)));
+ assertNull(
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.Distribution.create(BucketBoundaries.create(ImmutableList.of(3.15d))),
+ AggregationWindow.Cumulative.create()));
+ assertEquals(
+ MetricType.GAUGE,
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.LastValue.create(), AggregationWindow.Cumulative.create()));
+ assertEquals(
+ MetricType.GAUGE,
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.LastValue.create(), AggregationWindow.Interval.create(ONE_SECOND)));
+ }
+
+ @Test
+ public void createDimensionsWithNonMatchingListSizes() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("don't have the same size");
+ SignalFxSessionAdaptor.createDimensions(
+ ImmutableList.of(TagKey.create("animal"), TagKey.create("color")),
+ ImmutableList.of(TagValue.create("dog")));
+ }
+
+ @Test
+ public void createDimensionsIgnoresEmptyValues() {
+ List<Dimension> dimensions =
+ Lists.newArrayList(
+ SignalFxSessionAdaptor.createDimensions(
+ ImmutableList.of(TagKey.create("animal"), TagKey.create("color")),
+ ImmutableList.of(TagValue.create("dog"), TagValue.create(""))));
+ assertEquals(1, dimensions.size());
+ assertEquals("animal", dimensions.get(0).getKey());
+ assertEquals("dog", dimensions.get(0).getValue());
+ }
+
+ @Test
+ public void createDimension() {
+ Dimension dimension =
+ SignalFxSessionAdaptor.createDimension(TagKey.create("animal"), TagValue.create("dog"));
+ assertEquals("animal", dimension.getKey());
+ assertEquals("dog", dimension.getValue());
+ }
+
+ @Test
+ public void unsupportedAggregationYieldsNoDatapoints() {
+ Mockito.when(view.getAggregation())
+ .thenReturn(
+ Aggregation.Distribution.create(BucketBoundaries.create(ImmutableList.of(3.15d))));
+ Mockito.when(view.getWindow()).thenReturn(AggregationWindow.Cumulative.create());
+ List<DataPoint> datapoints = SignalFxSessionAdaptor.adapt(viewData);
+ assertEquals(0, datapoints.size());
+ }
+
+ @Test
+ public void noAggregationDataYieldsNoDatapoints() {
+ Mockito.when(view.getAggregation()).thenReturn(Aggregation.Count.create());
+ Mockito.when(view.getWindow()).thenReturn(AggregationWindow.Cumulative.create());
+ List<DataPoint> datapoints = SignalFxSessionAdaptor.adapt(viewData);
+ assertEquals(0, datapoints.size());
+ }
+
+ @Test
+ public void createDatumFromDoubleSum() {
+ SumDataDouble data = SumDataDouble.create(3.15d);
+ Datum datum = SignalFxSessionAdaptor.createDatum(data);
+ assertTrue(datum.hasDoubleValue());
+ assertFalse(datum.hasIntValue());
+ assertFalse(datum.hasStrValue());
+ assertEquals(3.15d, datum.getDoubleValue(), 0d);
+ }
+
+ @Test
+ public void createDatumFromLongSum() {
+ SumDataLong data = SumDataLong.create(42L);
+ Datum datum = SignalFxSessionAdaptor.createDatum(data);
+ assertFalse(datum.hasDoubleValue());
+ assertTrue(datum.hasIntValue());
+ assertFalse(datum.hasStrValue());
+ assertEquals(42L, datum.getIntValue());
+ }
+
+ @Test
+ public void createDatumFromCount() {
+ CountData data = CountData.create(42L);
+ Datum datum = SignalFxSessionAdaptor.createDatum(data);
+ assertFalse(datum.hasDoubleValue());
+ assertTrue(datum.hasIntValue());
+ assertFalse(datum.hasStrValue());
+ assertEquals(42L, datum.getIntValue());
+ }
+
+ @Test
+ public void createDatumFromMean() {
+ MeanData data = MeanData.create(3.15d, 2L);
+ Datum datum = SignalFxSessionAdaptor.createDatum(data);
+ assertTrue(datum.hasDoubleValue());
+ assertFalse(datum.hasIntValue());
+ assertFalse(datum.hasStrValue());
+ assertEquals(3.15d, datum.getDoubleValue(), 0d);
+ }
+
+ @Test
+ public void createDatumFromDistributionThrows() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Distribution aggregations are not supported");
+ SignalFxSessionAdaptor.createDatum(
+ DistributionData.create(5, 2, 0, 10, 40, ImmutableList.of(1L)));
+ }
+
+ @Test
+ public void createDatumFromLastValueDouble() {
+ LastValueDataDouble data = LastValueDataDouble.create(12.2);
+ Datum datum = SignalFxSessionAdaptor.createDatum(data);
+ assertTrue(datum.hasDoubleValue());
+ assertFalse(datum.hasIntValue());
+ assertFalse(datum.hasStrValue());
+ assertEquals(12.2, datum.getDoubleValue(), 0d);
+ }
+
+ @Test
+ public void createDatumFromLastValueLong() {
+ LastValueDataLong data = LastValueDataLong.create(100000);
+ Datum datum = SignalFxSessionAdaptor.createDatum(data);
+ assertFalse(datum.hasDoubleValue());
+ assertTrue(datum.hasIntValue());
+ assertFalse(datum.hasStrValue());
+ assertEquals(100000, datum.getIntValue());
+ }
+
+ @Test
+ public void adaptViewIntoDatapoints() {
+ Map<List<TagValue>, AggregationData> map =
+ ImmutableMap.<List<TagValue>, AggregationData>of(
+ ImmutableList.of(TagValue.create("dog")),
+ SumDataLong.create(2L),
+ ImmutableList.of(TagValue.create("cat")),
+ SumDataLong.create(3L));
+ Mockito.when(viewData.getAggregationMap()).thenReturn(map);
+ Mockito.when(view.getAggregation()).thenReturn(Aggregation.Count.create());
+ Mockito.when(view.getWindow()).thenReturn(AggregationWindow.Cumulative.create());
+
+ List<DataPoint> datapoints = SignalFxSessionAdaptor.adapt(viewData);
+ assertEquals(2, datapoints.size());
+ for (DataPoint dp : datapoints) {
+ assertEquals("view-name", dp.getMetric());
+ assertEquals(MetricType.CUMULATIVE_COUNTER, dp.getMetricType());
+ assertEquals(1, dp.getDimensionsCount());
+ assertTrue(dp.hasValue());
+ assertFalse(dp.hasSource());
+
+ Datum datum = dp.getValue();
+ assertTrue(datum.hasIntValue());
+ assertFalse(datum.hasDoubleValue());
+ assertFalse(datum.hasStrValue());
+
+ Dimension dimension = dp.getDimensions(0);
+ assertEquals("animal", dimension.getKey());
+ switch (dimension.getValue()) {
+ case "dog":
+ assertEquals(2L, datum.getIntValue());
+ break;
+ case "cat":
+ assertEquals(3L, datum.getIntValue());
+ break;
+ default:
+ fail("unexpected dimension value");
+ }
+ }
+ }
+
+ @Test
+ public void adaptViewWithEmptyTagValueIntoDatapoints() {
+ Map<List<TagValue>, AggregationData> map =
+ ImmutableMap.<List<TagValue>, AggregationData>of(
+ ImmutableList.of(TagValue.create("dog")),
+ SumDataLong.create(2L),
+ ImmutableList.of(TagValue.create("")),
+ SumDataLong.create(3L));
+ Mockito.when(viewData.getAggregationMap()).thenReturn(map);
+ Mockito.when(view.getAggregation()).thenReturn(Aggregation.Count.create());
+ Mockito.when(view.getWindow()).thenReturn(AggregationWindow.Cumulative.create());
+
+ List<DataPoint> datapoints = SignalFxSessionAdaptor.adapt(viewData);
+ assertEquals(2, datapoints.size());
+ for (DataPoint dp : datapoints) {
+ assertEquals("view-name", dp.getMetric());
+ assertEquals(MetricType.CUMULATIVE_COUNTER, dp.getMetricType());
+ assertTrue(dp.hasValue());
+ assertFalse(dp.hasSource());
+
+ Datum datum = dp.getValue();
+ assertTrue(datum.hasIntValue());
+ assertFalse(datum.hasDoubleValue());
+ assertFalse(datum.hasStrValue());
+
+ switch (dp.getDimensionsCount()) {
+ case 0:
+ assertEquals(3L, datum.getIntValue());
+ break;
+ case 1:
+ Dimension dimension = dp.getDimensions(0);
+ assertEquals("animal", dimension.getKey());
+ assertEquals("dog", dimension.getValue());
+ assertEquals(2L, datum.getIntValue());
+ break;
+ default:
+ fail("Unexpected number of dimensions on the created datapoint");
+ break;
+ }
+ }
+ }
+}
diff --git a/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfigurationTest.java b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfigurationTest.java
new file mode 100644
index 00000000..1d3508fb
--- /dev/null
+++ b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfigurationTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.stats.signalfx;
+
+import static org.junit.Assert.assertEquals;
+
+import io.opencensus.common.Duration;
+import java.net.URI;
+import java.net.URISyntaxException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SignalFxStatsConfiguration}. */
+@RunWith(JUnit4.class)
+public class SignalFxStatsConfigurationTest {
+
+ private static final String TEST_TOKEN = "token";
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void buildWithDefaults() {
+ SignalFxStatsConfiguration configuration =
+ SignalFxStatsConfiguration.builder().setToken(TEST_TOKEN).build();
+ assertEquals(TEST_TOKEN, configuration.getToken());
+ assertEquals(
+ SignalFxStatsConfiguration.DEFAULT_SIGNALFX_ENDPOINT, configuration.getIngestEndpoint());
+ assertEquals(
+ SignalFxStatsConfiguration.DEFAULT_EXPORT_INTERVAL, configuration.getExportInterval());
+ }
+
+ @Test
+ public void buildWithFields() throws URISyntaxException {
+ URI url = new URI("http://example.com");
+ Duration duration = Duration.create(5, 0);
+ SignalFxStatsConfiguration configuration =
+ SignalFxStatsConfiguration.builder()
+ .setToken(TEST_TOKEN)
+ .setIngestEndpoint(url)
+ .setExportInterval(duration)
+ .build();
+ assertEquals(TEST_TOKEN, configuration.getToken());
+ assertEquals(url, configuration.getIngestEndpoint());
+ assertEquals(duration, configuration.getExportInterval());
+ }
+
+ @Test
+ public void sameConfigurationsAreEqual() {
+ SignalFxStatsConfiguration config1 =
+ SignalFxStatsConfiguration.builder().setToken(TEST_TOKEN).build();
+ SignalFxStatsConfiguration config2 =
+ SignalFxStatsConfiguration.builder().setToken(TEST_TOKEN).build();
+ assertEquals(config1, config2);
+ assertEquals(config1.hashCode(), config2.hashCode());
+ }
+
+ @Test
+ public void buildWithEmptyToken() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Invalid SignalFx token");
+ SignalFxStatsConfiguration.builder().setToken("").build();
+ }
+
+ @Test
+ public void buildWithNegativeDuration() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Interval duration must be positive");
+ SignalFxStatsConfiguration.builder()
+ .setToken(TEST_TOKEN)
+ .setExportInterval(Duration.create(-1, 0))
+ .build();
+ }
+}
diff --git a/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterTest.java b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterTest.java
new file mode 100644
index 00000000..cc5730b1
--- /dev/null
+++ b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.stats.signalfx;
+
+import static org.junit.Assert.assertEquals;
+
+import io.opencensus.common.Duration;
+import java.net.URI;
+import java.net.URISyntaxException;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SignalFxStatsExporter}. */
+@RunWith(JUnit4.class)
+public class SignalFxStatsExporterTest {
+
+ private static final String TEST_TOKEN = "token";
+ private static final String TEST_ENDPOINT = "https://example.com";
+ private static final Duration ONE_SECOND = Duration.create(1, 0);
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @After
+ public void tearDown() {
+ SignalFxStatsExporter.unsafeResetExporter();
+ }
+
+ @Test
+ public void createWithNullConfiguration() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("configuration");
+ SignalFxStatsExporter.create(null);
+ }
+
+ @Test
+ public void createWithNullHostUsesDefault() {
+ SignalFxStatsExporter.create(SignalFxStatsConfiguration.builder().setToken(TEST_TOKEN).build());
+ assertEquals(
+ SignalFxStatsConfiguration.DEFAULT_SIGNALFX_ENDPOINT,
+ SignalFxStatsExporter.unsafeGetConfig().getIngestEndpoint());
+ }
+
+ @Test
+ public void createWithNullIntervalUsesDefault() {
+ SignalFxStatsExporter.create(SignalFxStatsConfiguration.builder().setToken(TEST_TOKEN).build());
+ assertEquals(
+ SignalFxStatsConfiguration.DEFAULT_EXPORT_INTERVAL,
+ SignalFxStatsExporter.unsafeGetConfig().getExportInterval());
+ }
+
+ @Test
+ public void createExporterTwice() {
+ SignalFxStatsConfiguration config =
+ SignalFxStatsConfiguration.builder()
+ .setToken(TEST_TOKEN)
+ .setExportInterval(ONE_SECOND)
+ .build();
+ SignalFxStatsExporter.create(config);
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("SignalFx stats exporter is already created.");
+ SignalFxStatsExporter.create(config);
+ }
+
+ @Test
+ public void createWithConfiguration() throws URISyntaxException {
+ SignalFxStatsConfiguration config =
+ SignalFxStatsConfiguration.builder()
+ .setToken(TEST_TOKEN)
+ .setIngestEndpoint(new URI(TEST_ENDPOINT))
+ .setExportInterval(ONE_SECOND)
+ .build();
+ SignalFxStatsExporter.create(config);
+ assertEquals(config, SignalFxStatsExporter.unsafeGetConfig());
+ }
+}
diff --git a/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThreadTest.java b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThreadTest.java
new file mode 100644
index 00000000..d8852d5f
--- /dev/null
+++ b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThreadTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.stats.signalfx;
+
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.signalfx.metrics.errorhandler.OnSendErrorHandler;
+import com.signalfx.metrics.flush.AggregateMetricSender;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.DataPoint;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Datum;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Dimension;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.MetricType;
+import io.opencensus.common.Duration;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow;
+import io.opencensus.stats.View.Name;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SignalFxStatsExporterWorkerThreadTest {
+
+ private static final String TEST_TOKEN = "token";
+ private static final Duration ONE_SECOND = Duration.create(1, 0);
+
+ @Mock private AggregateMetricSender.Session session;
+
+ @Mock private ViewManager viewManager;
+
+ @Mock private SignalFxMetricsSenderFactory factory;
+
+ private URI endpoint;
+
+ @Before
+ public void setUp() throws Exception {
+ endpoint = new URI("http://example.com");
+
+ Mockito.when(
+ factory.create(
+ Mockito.any(URI.class), Mockito.anyString(), Mockito.any(OnSendErrorHandler.class)))
+ .thenAnswer(
+ new Answer<AggregateMetricSender>() {
+ @Override
+ public AggregateMetricSender answer(InvocationOnMock invocation) {
+ Object[] args = invocation.getArguments();
+ AggregateMetricSender sender =
+ SignalFxMetricsSenderFactory.DEFAULT.create(
+ (URI) args[0], (String) args[1], (OnSendErrorHandler) args[2]);
+ AggregateMetricSender spy = Mockito.spy(sender);
+ Mockito.doReturn(session).when(spy).createSession();
+ return spy;
+ }
+ });
+ }
+
+ @Test
+ public void createThread() {
+ SignalFxStatsExporterWorkerThread thread =
+ new SignalFxStatsExporterWorkerThread(
+ factory, endpoint, TEST_TOKEN, ONE_SECOND, viewManager);
+ assertTrue(thread.isDaemon());
+ assertThat(thread.getName(), startsWith("SignalFx"));
+ }
+
+ @Test
+ public void senderThreadInterruptStopsLoop() throws InterruptedException {
+ Mockito.when(session.setDatapoint(Mockito.any(DataPoint.class))).thenReturn(session);
+ Mockito.when(viewManager.getAllExportedViews()).thenReturn(ImmutableSet.<View>of());
+
+ SignalFxStatsExporterWorkerThread thread =
+ new SignalFxStatsExporterWorkerThread(
+ factory, endpoint, TEST_TOKEN, ONE_SECOND, viewManager);
+ thread.start();
+ thread.interrupt();
+ thread.join(5000, 0);
+ assertFalse("Worker thread should have stopped", thread.isAlive());
+ }
+
+ @Test
+ public void setsDatapointsFromViewOnSession() throws IOException {
+ View view = Mockito.mock(View.class);
+ Name viewName = Name.create("test");
+ Mockito.when(view.getName()).thenReturn(viewName);
+ Mockito.when(view.getAggregation()).thenReturn(Aggregation.Mean.create());
+ Mockito.when(view.getWindow()).thenReturn(AggregationWindow.Cumulative.create());
+ Mockito.when(view.getColumns()).thenReturn(ImmutableList.of(TagKey.create("animal")));
+
+ ViewData viewData = Mockito.mock(ViewData.class);
+ Mockito.when(viewData.getView()).thenReturn(view);
+ Mockito.when(viewData.getAggregationMap())
+ .thenReturn(
+ ImmutableMap.<List<TagValue>, AggregationData>of(
+ ImmutableList.of(TagValue.create("cat")), MeanData.create(3.15d, 1)));
+
+ Mockito.when(viewManager.getAllExportedViews()).thenReturn(ImmutableSet.of(view));
+ Mockito.when(viewManager.getView(Mockito.eq(viewName))).thenReturn(viewData);
+
+ SignalFxStatsExporterWorkerThread thread =
+ new SignalFxStatsExporterWorkerThread(
+ factory, endpoint, TEST_TOKEN, ONE_SECOND, viewManager);
+ thread.export();
+
+ DataPoint datapoint =
+ DataPoint.newBuilder()
+ .setMetric("test")
+ .setMetricType(MetricType.GAUGE)
+ .addDimensions(Dimension.newBuilder().setKey("animal").setValue("cat").build())
+ .setValue(Datum.newBuilder().setDoubleValue(3.15d).build())
+ .build();
+ Mockito.verify(session).setDatapoint(Mockito.eq(datapoint));
+ Mockito.verify(session).close();
+ }
+}
diff --git a/exporters/stats/stackdriver/README.md b/exporters/stats/stackdriver/README.md
new file mode 100644
index 00000000..1b35c635
--- /dev/null
+++ b/exporters/stats/stackdriver/README.md
@@ -0,0 +1,171 @@
+# OpenCensus Stackdriver Stats Exporter
+
+The *OpenCensus Stackdriver Stats Exporter* is a stats exporter that exports data to
+Stackdriver Monitoring. [Stackdriver Monitoring][stackdriver-monitoring] provides visibility into
+the performance, uptime, and overall health of cloud-powered applications. Stackdriver ingests that
+data and generates insights via dashboards, charts, and alerts.
+
+## Quickstart
+
+### Prerequisites
+
+To use this exporter, you must have an application that you'd like to monitor. The app can be on
+Google Cloud Platform, on-premise, or another cloud platform.
+
+In order to be able to push your stats to [Stackdriver Monitoring][stackdriver-monitoring], 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 Monitoring API](https://console.cloud.google.com/apis/dashboard).
+
+These steps enable the API but don't require that your app is hosted on Google Cloud Platform.
+
+### Hello "Stackdriver Stats"
+
+#### 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-stats-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-stats-stackdriver:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### Register the exporter
+
+This uses the default configuration for authentication and a given project ID.
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) {
+ StackdriverStatsExporter.createAndRegister(
+ StackdriverStatsConfiguration.builder().build());
+ }
+}
+```
+
+#### Set Monitored Resource for exporter
+
+By default, Stackdriver Stats Exporter will try to automatically detect the environment if your
+application is running on GCE, GKE or AWS EC2, and generate a corresponding Stackdriver GCE/GKE/EC2
+monitored resource. For GKE particularly, you may want to set up some environment variables so that
+Exporter can correctly identify your pod, cluster and container. Follow the Kubernetes instruction
+[here](https://cloud.google.com/kubernetes-engine/docs/tutorials/custom-metrics-autoscaling#exporting_metrics_from_the_application)
+and [here](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/).
+
+Otherwise, Exporter will use [a global Stackdriver monitored resource with a project_id label](https://cloud.google.com/monitoring/api/resources#tag_global),
+and it works fine when you have only one exporter running.
+
+If you want to have multiple processes exporting stats for the same metric concurrently, and your
+application is running on some different environment than GCE, GKE or AWS EC2 (for example DataFlow),
+please associate a unique monitored resource with each exporter if possible.
+Please note that there is also an "opencensus_task" metric label that uniquely identifies the
+uploaded stats.
+
+To set a custom MonitoredResource:
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) {
+ // A sample DataFlow monitored resource.
+ MonitoredResource myResource = MonitoredResource.newBuilder()
+ .setType("dataflow_job")
+ .putLabels("project_id", "my_project")
+ .putLabels("job_name", "my_job")
+ .putLabels("region", "us-east1")
+ .build();
+
+ // Set a custom MonitoredResource. Please make sure each Stackdriver Stats Exporter has a
+ // unique MonitoredResource.
+ StackdriverStatsExporter.createAndRegister(
+ StackdriverStatsConfiguration.builder().setMonitoredResource(myResource).build());
+ }
+}
+```
+
+For a complete list of valid Stackdriver monitored resources, please refer to [Stackdriver
+Documentation](https://cloud.google.com/monitoring/custom-metrics/creating-metrics#which-resource).
+Please also note that although there are a lot of monitored resources available on [Stackdriver](https://cloud.google.com/monitoring/api/resources),
+only [a small subset of them](https://cloud.google.com/monitoring/custom-metrics/creating-metrics#which-resource)
+are compatible with the Opencensus Stackdriver Stats Exporter.
+
+#### 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:
+```
+StackdriverStatsExporter.createAndRegister(
+ StackdriverStatsConfiguration.builder()
+ .setCredentials(new GoogleCredentials(new AccessToken(accessToken, expirationTime)))
+ .setProjectId("MyStackdriverProjectId")
+ .setExportInterval(Duration.create(10, 0))
+ .build());
+```
+
+#### 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:
+```
+StackdriverStatsExporter.createAndRegister(
+ StackdriverStatsConfiguration.builder().setProjectId("MyStackdriverProjectId").build());
+```
+
+#### Java Versions
+
+Java 7 or above is required for using this exporter.
+
+## FAQ
+### Why did I get a PERMISSION_DENIED error from Stackdriver when using this exporter?
+To use our Stackdriver Stats exporter, you need to set up billing for your cloud project, since
+creating and uploading custom metrics to Stackdriver Monitoring is
+[not free](https://cloud.google.com/stackdriver/pricing_v2#monitoring-costs).
+
+To enable billing, follow the instructions [here](https://support.google.com/cloud/answer/6288653#new-billing).
+
+### What is "opencensus_task" metric label ?
+Stackdriver requires that each Timeseries to be updated only by one task at a time. A
+`Timeseries` is uniquely identified by the `MonitoredResource` and the `Metric`'s labels.
+Stackdriver exporter adds a new `Metric` label for each custom `Metric` to ensure the uniqueness
+of the `Timeseries`. The format of the label is: `{LANGUAGE}-{PID}@{HOSTNAME}`, if `{PID}` is not
+available a random number will be used.
+
+### Why did I get an error "java.lang.NoSuchMethodError: com.google.common...", like "java.lang.NoSuchMethodError:com.google.common.base.Throwables.throwIfInstanceOf"?
+This is probably because there is a version conflict on Guava in the dependency tree.
+
+For example, `com.google.common.base.Throwables.throwIfInstanceOf` is introduced to Guava 20.0.
+If your application has a dependency that bundles a Guava with version 19.0 or below
+(for example, gRPC 1.10.0), it might cause a `NoSuchMethodError` since
+`com.google.common.base.Throwables.throwIfInstanceOf` doesn't exist before Guava 20.0.
+
+In this case, please either add an explicit dependency on a newer version of Guava that has the
+new method (20.0 in the previous example), or if possible, upgrade the dependency that depends on
+Guava to a newer version that depends on the newer Guava (for example, upgrade to gRPC 1.12.0).
+
+[stackdriver-monitoring]: https://cloud.google.com/monitoring/
diff --git a/exporters/stats/stackdriver/build.gradle b/exporters/stats/stackdriver/build.gradle
new file mode 100644
index 00000000..0bc302a6
--- /dev/null
+++ b/exporters/stats/stackdriver/build.gradle
@@ -0,0 +1,30 @@
+description = 'OpenCensus Stats 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_monitoring) {
+ // 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"
+} \ No newline at end of file
diff --git a/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtils.java b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtils.java
new file mode 100644
index 00000000..4f8715b0
--- /dev/null
+++ b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtils.java
@@ -0,0 +1,518 @@
+/*
+ * 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.stats.stackdriver;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.api.Distribution;
+import com.google.api.Distribution.BucketOptions;
+import com.google.api.Distribution.BucketOptions.Explicit;
+import com.google.api.LabelDescriptor;
+import com.google.api.LabelDescriptor.ValueType;
+import com.google.api.Metric;
+import com.google.api.MetricDescriptor;
+import com.google.api.MetricDescriptor.MetricKind;
+import com.google.api.MonitoredResource;
+import com.google.cloud.MetadataConfig;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.monitoring.v3.Point;
+import com.google.monitoring.v3.TimeInterval;
+import com.google.monitoring.v3.TimeSeries;
+import com.google.monitoring.v3.TypedValue;
+import com.google.monitoring.v3.TypedValue.Builder;
+import com.google.protobuf.Timestamp;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+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.stats.Aggregation;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.lang.management.ManagementFactory;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.SecureRandom;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Util methods to convert OpenCensus Stats data models to StackDriver monitoring data models. */
+@SuppressWarnings("deprecation")
+final class StackdriverExportUtils {
+
+ // TODO(songya): do we want these constants to be customizable?
+ @VisibleForTesting static final String LABEL_DESCRIPTION = "OpenCensus TagKey";
+ @VisibleForTesting static final String OPENCENSUS_TASK = "opencensus_task";
+ @VisibleForTesting static final String OPENCENSUS_TASK_DESCRIPTION = "Opencensus task identifier";
+ private static final String GCP_GKE_CONTAINER = "k8s_container";
+ private static final String GCP_GCE_INSTANCE = "gce_instance";
+ private static final String AWS_EC2_INSTANCE = "aws_ec2_instance";
+ private static final String GLOBAL = "global";
+
+ private static final Logger logger = Logger.getLogger(StackdriverExportUtils.class.getName());
+ private static final String OPENCENSUS_TASK_VALUE_DEFAULT = generateDefaultTaskValue();
+ private static final String PROJECT_ID_LABEL_KEY = "project_id";
+
+ // Constant functions for ValueType.
+ private static final Function<Object, MetricDescriptor.ValueType> VALUE_TYPE_DOUBLE_FUNCTION =
+ Functions.returnConstant(MetricDescriptor.ValueType.DOUBLE);
+ private static final Function<Object, MetricDescriptor.ValueType> VALUE_TYPE_INT64_FUNCTION =
+ Functions.returnConstant(MetricDescriptor.ValueType.INT64);
+ private static final Function<Object, MetricDescriptor.ValueType>
+ VALUE_TYPE_UNRECOGNIZED_FUNCTION =
+ Functions.returnConstant(MetricDescriptor.ValueType.UNRECOGNIZED);
+ private static final Function<Object, MetricDescriptor.ValueType>
+ VALUE_TYPE_DISTRIBUTION_FUNCTION =
+ Functions.returnConstant(MetricDescriptor.ValueType.DISTRIBUTION);
+ private static final Function<Aggregation, MetricDescriptor.ValueType> valueTypeMeanFunction =
+ new Function<Aggregation, MetricDescriptor.ValueType>() {
+ @Override
+ public MetricDescriptor.ValueType apply(Aggregation arg) {
+ // TODO(songya): remove this once Mean aggregation is completely removed. Before that
+ // we need to continue supporting Mean, since it could still be used by users and some
+ // deprecated RPC views.
+ if (arg instanceof Aggregation.Mean) {
+ return MetricDescriptor.ValueType.DOUBLE;
+ }
+ return MetricDescriptor.ValueType.UNRECOGNIZED;
+ }
+ };
+
+ // Constant functions for MetricKind.
+ private static final Function<Object, MetricKind> METRIC_KIND_CUMULATIVE_FUNCTION =
+ Functions.returnConstant(MetricKind.CUMULATIVE);
+ private static final Function<Object, MetricKind> METRIC_KIND_UNRECOGNIZED_FUNCTION =
+ Functions.returnConstant(MetricKind.UNRECOGNIZED);
+
+ // Constant functions for TypedValue.
+ private static final Function<SumDataDouble, TypedValue> typedValueSumDoubleFunction =
+ new Function<SumDataDouble, TypedValue>() {
+ @Override
+ public TypedValue apply(SumDataDouble arg) {
+ Builder builder = TypedValue.newBuilder();
+ builder.setDoubleValue(arg.getSum());
+ return builder.build();
+ }
+ };
+ private static final Function<SumDataLong, TypedValue> typedValueSumLongFunction =
+ new Function<SumDataLong, TypedValue>() {
+ @Override
+ public TypedValue apply(SumDataLong arg) {
+ Builder builder = TypedValue.newBuilder();
+ builder.setInt64Value(arg.getSum());
+ return builder.build();
+ }
+ };
+ private static final Function<CountData, TypedValue> typedValueCountFunction =
+ new Function<CountData, TypedValue>() {
+ @Override
+ public TypedValue apply(CountData arg) {
+ Builder builder = TypedValue.newBuilder();
+ builder.setInt64Value(arg.getCount());
+ return builder.build();
+ }
+ };
+ private static final Function<LastValueDataDouble, TypedValue> typedValueLastValueDoubleFunction =
+ new Function<LastValueDataDouble, TypedValue>() {
+ @Override
+ public TypedValue apply(LastValueDataDouble arg) {
+ Builder builder = TypedValue.newBuilder();
+ builder.setDoubleValue(arg.getLastValue());
+ return builder.build();
+ }
+ };
+ private static final Function<LastValueDataLong, TypedValue> typedValueLastValueLongFunction =
+ new Function<LastValueDataLong, TypedValue>() {
+ @Override
+ public TypedValue apply(LastValueDataLong arg) {
+ Builder builder = TypedValue.newBuilder();
+ builder.setInt64Value(arg.getLastValue());
+ return builder.build();
+ }
+ };
+ private static final Function<AggregationData, TypedValue> typedValueMeanFunction =
+ new Function<AggregationData, TypedValue>() {
+ @Override
+ public TypedValue apply(AggregationData arg) {
+ Builder builder = TypedValue.newBuilder();
+ // TODO(songya): remove this once Mean aggregation is completely removed. Before that
+ // we need to continue supporting Mean, since it could still be used by users and some
+ // deprecated RPC views.
+ if (arg instanceof AggregationData.MeanData) {
+ builder.setDoubleValue(((AggregationData.MeanData) arg).getMean());
+ return builder.build();
+ }
+ throw new IllegalArgumentException("Unknown Aggregation");
+ }
+ };
+
+ private static String generateDefaultTaskValue() {
+ // Something like '<pid>@<hostname>', at least in Oracle and OpenJdk JVMs
+ final String jvmName = ManagementFactory.getRuntimeMXBean().getName();
+ // If not the expected format then generate a random number.
+ if (jvmName.indexOf('@') < 1) {
+ String hostname = "localhost";
+ try {
+ hostname = InetAddress.getLocalHost().getHostName();
+ } catch (UnknownHostException e) {
+ logger.log(Level.INFO, "Unable to get the hostname.", e);
+ }
+ // Generate a random number and use the same format "random_number@hostname".
+ return "java-" + new SecureRandom().nextInt() + "@" + hostname;
+ }
+ return "java-" + jvmName;
+ }
+
+ // Construct a MetricDescriptor using a View.
+ @javax.annotation.Nullable
+ static MetricDescriptor createMetricDescriptor(
+ View view, String projectId, String domain, String displayNamePrefix) {
+ if (!(view.getWindow() instanceof View.AggregationWindow.Cumulative)) {
+ // TODO(songya): Only Cumulative view will be exported to Stackdriver in this version.
+ return null;
+ }
+
+ MetricDescriptor.Builder builder = MetricDescriptor.newBuilder();
+ String viewName = view.getName().asString();
+ String type = generateType(viewName, domain);
+ // Name format refers to
+ // cloud.google.com/monitoring/api/ref_v3/rest/v3/projects.metricDescriptors/create
+ builder.setName(String.format("projects/%s/metricDescriptors/%s", projectId, type));
+ builder.setType(type);
+ builder.setDescription(view.getDescription());
+ String displayName = createDisplayName(viewName, displayNamePrefix);
+ builder.setDisplayName(displayName);
+ for (TagKey tagKey : view.getColumns()) {
+ builder.addLabels(createLabelDescriptor(tagKey));
+ }
+ builder.addLabels(
+ LabelDescriptor.newBuilder()
+ .setKey(OPENCENSUS_TASK)
+ .setDescription(OPENCENSUS_TASK_DESCRIPTION)
+ .setValueType(ValueType.STRING)
+ .build());
+ builder.setUnit(createUnit(view.getAggregation(), view.getMeasure()));
+ builder.setMetricKind(createMetricKind(view.getWindow(), view.getAggregation()));
+ builder.setValueType(createValueType(view.getAggregation(), view.getMeasure()));
+ return builder.build();
+ }
+
+ private static String generateType(String viewName, String domain) {
+ return domain + viewName;
+ }
+
+ private static String createDisplayName(String viewName, String displayNamePrefix) {
+ return displayNamePrefix + viewName;
+ }
+
+ // Construct a LabelDescriptor from a TagKey
+ @VisibleForTesting
+ static LabelDescriptor createLabelDescriptor(TagKey tagKey) {
+ LabelDescriptor.Builder builder = LabelDescriptor.newBuilder();
+ builder.setKey(tagKey.getName());
+ builder.setDescription(LABEL_DESCRIPTION);
+ // Now we only support String tags
+ builder.setValueType(ValueType.STRING);
+ return builder.build();
+ }
+
+ // Construct a MetricKind from an AggregationWindow
+ @VisibleForTesting
+ static MetricKind createMetricKind(View.AggregationWindow window, Aggregation aggregation) {
+ if (aggregation instanceof LastValue) {
+ return MetricKind.GAUGE;
+ }
+ return window.match(
+ METRIC_KIND_CUMULATIVE_FUNCTION, // Cumulative
+ // TODO(songya): We don't support exporting Interval stats to StackDriver in this version.
+ METRIC_KIND_UNRECOGNIZED_FUNCTION, // Interval
+ METRIC_KIND_UNRECOGNIZED_FUNCTION);
+ }
+
+ // Construct a MetricDescriptor.ValueType from an Aggregation and a Measure
+ @VisibleForTesting
+ static String createUnit(Aggregation aggregation, final Measure measure) {
+ if (aggregation instanceof Aggregation.Count) {
+ return "1";
+ }
+ return measure.getUnit();
+ }
+
+ // Construct a MetricDescriptor.ValueType from an Aggregation and a Measure
+ @VisibleForTesting
+ static MetricDescriptor.ValueType createValueType(
+ Aggregation aggregation, final Measure measure) {
+ return aggregation.match(
+ Functions.returnConstant(
+ measure.match(
+ VALUE_TYPE_DOUBLE_FUNCTION, // Sum Double
+ VALUE_TYPE_INT64_FUNCTION, // Sum Long
+ VALUE_TYPE_UNRECOGNIZED_FUNCTION)),
+ VALUE_TYPE_INT64_FUNCTION, // Count
+ VALUE_TYPE_DISTRIBUTION_FUNCTION, // Distribution
+ Functions.returnConstant(
+ measure.match(
+ VALUE_TYPE_DOUBLE_FUNCTION, // LastValue Double
+ VALUE_TYPE_INT64_FUNCTION, // LastValue Long
+ VALUE_TYPE_UNRECOGNIZED_FUNCTION)),
+ valueTypeMeanFunction);
+ }
+
+ // Convert ViewData to a list of TimeSeries, so that ViewData can be uploaded to Stackdriver.
+ static List<TimeSeries> createTimeSeriesList(
+ @javax.annotation.Nullable ViewData viewData,
+ MonitoredResource monitoredResource,
+ String domain) {
+ List<TimeSeries> timeSeriesList = Lists.newArrayList();
+ if (viewData == null) {
+ return timeSeriesList;
+ }
+ View view = viewData.getView();
+ if (!(view.getWindow() instanceof View.AggregationWindow.Cumulative)) {
+ // TODO(songya): Only Cumulative view will be exported to Stackdriver in this version.
+ return timeSeriesList;
+ }
+
+ // Shared fields for all TimeSeries generated from the same ViewData
+ TimeSeries.Builder shared = TimeSeries.newBuilder();
+ shared.setMetricKind(createMetricKind(view.getWindow(), view.getAggregation()));
+ shared.setResource(monitoredResource);
+ shared.setValueType(createValueType(view.getAggregation(), view.getMeasure()));
+
+ // Each entry in AggregationMap will be converted into an independent TimeSeries object
+ for (Entry<List</*@Nullable*/ TagValue>, AggregationData> entry :
+ viewData.getAggregationMap().entrySet()) {
+ TimeSeries.Builder builder = shared.clone();
+ builder.setMetric(createMetric(view, entry.getKey(), domain));
+ builder.addPoints(
+ createPoint(entry.getValue(), viewData.getWindowData(), view.getAggregation()));
+ timeSeriesList.add(builder.build());
+ }
+
+ return timeSeriesList;
+ }
+
+ // Create a Metric using the TagKeys and TagValues.
+ @VisibleForTesting
+ static Metric createMetric(View view, List</*@Nullable*/ TagValue> tagValues, String domain) {
+ Metric.Builder builder = Metric.newBuilder();
+ // TODO(songya): use pre-defined metrics for canonical views
+ builder.setType(generateType(view.getName().asString(), domain));
+ Map<String, String> stringTagMap = Maps.newHashMap();
+ List<TagKey> columns = view.getColumns();
+ checkArgument(
+ tagValues.size() == columns.size(), "TagKeys and TagValues don't have same size.");
+ for (int i = 0; i < tagValues.size(); i++) {
+ TagKey key = columns.get(i);
+ TagValue value = tagValues.get(i);
+ if (value == null) {
+ continue;
+ }
+ stringTagMap.put(key.getName(), value.asString());
+ }
+ stringTagMap.put(OPENCENSUS_TASK, OPENCENSUS_TASK_VALUE_DEFAULT);
+ builder.putAllLabels(stringTagMap);
+ return builder.build();
+ }
+
+ // Create Point from AggregationData, AggregationWindowData and Aggregation.
+ @VisibleForTesting
+ static Point createPoint(
+ AggregationData aggregationData,
+ ViewData.AggregationWindowData windowData,
+ Aggregation aggregation) {
+ Point.Builder builder = Point.newBuilder();
+ builder.setInterval(createTimeInterval(windowData, aggregation));
+ builder.setValue(createTypedValue(aggregation, aggregationData));
+ return builder.build();
+ }
+
+ // Convert AggregationWindowData to TimeInterval, currently only support CumulativeData.
+ @VisibleForTesting
+ static TimeInterval createTimeInterval(
+ ViewData.AggregationWindowData windowData, final Aggregation aggregation) {
+ return windowData.match(
+ new Function<ViewData.AggregationWindowData.CumulativeData, TimeInterval>() {
+ @Override
+ public TimeInterval apply(ViewData.AggregationWindowData.CumulativeData arg) {
+ TimeInterval.Builder builder = TimeInterval.newBuilder();
+ builder.setEndTime(convertTimestamp(arg.getEnd()));
+ if (!(aggregation instanceof LastValue)) {
+ builder.setStartTime(convertTimestamp(arg.getStart()));
+ }
+ return builder.build();
+ }
+ },
+ Functions.<TimeInterval>throwIllegalArgumentException(),
+ Functions.<TimeInterval>throwIllegalArgumentException());
+ }
+
+ // Create a TypedValue using AggregationData and Aggregation
+ // Note TypedValue is "A single strongly-typed value", i.e only one field should be set.
+ @VisibleForTesting
+ static TypedValue createTypedValue(
+ final Aggregation aggregation, AggregationData aggregationData) {
+ return aggregationData.match(
+ typedValueSumDoubleFunction,
+ typedValueSumLongFunction,
+ typedValueCountFunction,
+ new Function<DistributionData, TypedValue>() {
+ @Override
+ public TypedValue apply(DistributionData arg) {
+ TypedValue.Builder builder = TypedValue.newBuilder();
+ checkArgument(
+ aggregation instanceof Aggregation.Distribution,
+ "Aggregation and AggregationData mismatch.");
+ builder.setDistributionValue(
+ createDistribution(
+ arg, ((Aggregation.Distribution) aggregation).getBucketBoundaries()));
+ return builder.build();
+ }
+ },
+ typedValueLastValueDoubleFunction,
+ typedValueLastValueLongFunction,
+ typedValueMeanFunction);
+ }
+
+ // Create a StackDriver Distribution from DistributionData and BucketBoundaries
+ @VisibleForTesting
+ static Distribution createDistribution(
+ DistributionData distributionData, BucketBoundaries bucketBoundaries) {
+ return Distribution.newBuilder()
+ .setBucketOptions(createBucketOptions(bucketBoundaries))
+ .addAllBucketCounts(distributionData.getBucketCounts())
+ .setCount(distributionData.getCount())
+ .setMean(distributionData.getMean())
+ // TODO(songya): uncomment this once Stackdriver supports setting max and min.
+ // .setRange(
+ // Range.newBuilder()
+ // .setMax(distributionData.getMax())
+ // .setMin(distributionData.getMin())
+ // .build())
+ .setSumOfSquaredDeviation(distributionData.getSumOfSquaredDeviations())
+ .build();
+ }
+
+ // Create BucketOptions from BucketBoundaries
+ @VisibleForTesting
+ static BucketOptions createBucketOptions(BucketBoundaries bucketBoundaries) {
+ return BucketOptions.newBuilder()
+ .setExplicitBuckets(Explicit.newBuilder().addAllBounds(bucketBoundaries.getBoundaries()))
+ .build();
+ }
+
+ // Convert a Census Timestamp to a StackDriver Timestamp
+ @VisibleForTesting
+ static Timestamp convertTimestamp(io.opencensus.common.Timestamp censusTimestamp) {
+ if (censusTimestamp.getSeconds() < 0) {
+ // Stackdriver doesn't handle negative timestamps.
+ return Timestamp.newBuilder().build();
+ }
+ return Timestamp.newBuilder()
+ .setSeconds(censusTimestamp.getSeconds())
+ .setNanos(censusTimestamp.getNanos())
+ .build();
+ }
+
+ /* Return a self-configured Stackdriver monitored resource. */
+ static MonitoredResource getDefaultResource() {
+ MonitoredResource.Builder builder = MonitoredResource.newBuilder();
+ io.opencensus.contrib.monitoredresource.util.MonitoredResource autoDetectedResource =
+ MonitoredResourceUtils.getDefaultResource();
+ if (autoDetectedResource == null) {
+ builder.setType(GLOBAL);
+ if (MetadataConfig.getProjectId() != null) {
+ // For default global resource, always use the project id from MetadataConfig. This allows
+ // stats from other projects (e.g from GAE running in another project) to be collected.
+ builder.putLabels(PROJECT_ID_LABEL_KEY, MetadataConfig.getProjectId());
+ }
+ return builder.build();
+ }
+ builder.setType(mapToStackdriverResourceType(autoDetectedResource.getResourceType()));
+ setMonitoredResourceLabelsForBuilder(builder, autoDetectedResource);
+ return builder.build();
+ }
+
+ private static String mapToStackdriverResourceType(ResourceType resourceType) {
+ switch (resourceType) {
+ case GCP_GCE_INSTANCE:
+ return GCP_GCE_INSTANCE;
+ case GCP_GKE_CONTAINER:
+ return GCP_GKE_CONTAINER;
+ case AWS_EC2_INSTANCE:
+ return AWS_EC2_INSTANCE;
+ }
+ throw new IllegalArgumentException("Unknown resource type.");
+ }
+
+ private static void setMonitoredResourceLabelsForBuilder(
+ MonitoredResource.Builder builder,
+ io.opencensus.contrib.monitoredresource.util.MonitoredResource autoDetectedResource) {
+ switch (autoDetectedResource.getResourceType()) {
+ case GCP_GCE_INSTANCE:
+ GcpGceInstanceMonitoredResource gcpGceInstanceMonitoredResource =
+ (GcpGceInstanceMonitoredResource) autoDetectedResource;
+ builder.putLabels(PROJECT_ID_LABEL_KEY, gcpGceInstanceMonitoredResource.getAccount());
+ builder.putLabels("instance_id", gcpGceInstanceMonitoredResource.getInstanceId());
+ builder.putLabels("zone", gcpGceInstanceMonitoredResource.getZone());
+ return;
+ case GCP_GKE_CONTAINER:
+ GcpGkeContainerMonitoredResource gcpGkeContainerMonitoredResource =
+ (GcpGkeContainerMonitoredResource) autoDetectedResource;
+ builder.putLabels(PROJECT_ID_LABEL_KEY, gcpGkeContainerMonitoredResource.getAccount());
+ builder.putLabels("cluster_name", gcpGkeContainerMonitoredResource.getClusterName());
+ builder.putLabels("container_name", gcpGkeContainerMonitoredResource.getContainerName());
+ builder.putLabels("namespace_name", gcpGkeContainerMonitoredResource.getNamespaceId());
+ builder.putLabels("pod_name", gcpGkeContainerMonitoredResource.getPodId());
+ builder.putLabels("location", gcpGkeContainerMonitoredResource.getZone());
+ return;
+ case AWS_EC2_INSTANCE:
+ AwsEc2InstanceMonitoredResource awsEc2InstanceMonitoredResource =
+ (AwsEc2InstanceMonitoredResource) autoDetectedResource;
+ builder.putLabels("aws_account", awsEc2InstanceMonitoredResource.getAccount());
+ builder.putLabels("instance_id", awsEc2InstanceMonitoredResource.getInstanceId());
+ builder.putLabels("region", "aws:" + awsEc2InstanceMonitoredResource.getRegion());
+ return;
+ }
+ throw new IllegalArgumentException("Unknown subclass of MonitoredResource.");
+ }
+
+ private StackdriverExportUtils() {}
+}
diff --git a/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorker.java b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorker.java
new file mode 100644
index 00000000..5ffed9d5
--- /dev/null
+++ b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorker.java
@@ -0,0 +1,274 @@
+/*
+ * 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.stats.stackdriver;
+
+import com.google.api.MetricDescriptor;
+import com.google.api.MonitoredResource;
+import com.google.api.gax.rpc.ApiException;
+import com.google.cloud.monitoring.v3.MetricServiceClient;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.monitoring.v3.CreateMetricDescriptorRequest;
+import com.google.monitoring.v3.CreateTimeSeriesRequest;
+import com.google.monitoring.v3.ProjectName;
+import com.google.monitoring.v3.TimeSeries;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Scope;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.concurrent.NotThreadSafe;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Worker {@code Runnable} that polls ViewData from Stats library and batch export to StackDriver.
+ *
+ * <p>{@code StackdriverExporterWorker} will be started in a daemon {@code Thread}.
+ *
+ * <p>The state of this class should only be accessed from the thread which {@link
+ * StackdriverExporterWorker} resides in.
+ */
+@NotThreadSafe
+final class StackdriverExporterWorker implements Runnable {
+
+ private static final Logger logger = Logger.getLogger(StackdriverExporterWorker.class.getName());
+
+ // Stackdriver Monitoring v3 only accepts up to 200 TimeSeries per CreateTimeSeries call.
+ @VisibleForTesting static final int MAX_BATCH_EXPORT_SIZE = 200;
+
+ @VisibleForTesting static final String DEFAULT_DISPLAY_NAME_PREFIX = "OpenCensus/";
+ @VisibleForTesting static final String CUSTOM_METRIC_DOMAIN = "custom.googleapis.com/";
+
+ @VisibleForTesting
+ static final String CUSTOM_OPENCENSUS_DOMAIN = CUSTOM_METRIC_DOMAIN + "opencensus/";
+
+ private final long scheduleDelayMillis;
+ private final String projectId;
+ private final ProjectName projectName;
+ private final MetricServiceClient metricServiceClient;
+ private final ViewManager viewManager;
+ private final MonitoredResource monitoredResource;
+ private final String domain;
+ private final String displayNamePrefix;
+ private final Map<View.Name, View> registeredViews = new HashMap<View.Name, View>();
+
+ private static final Tracer tracer = Tracing.getTracer();
+ private static final Sampler probabilitySampler = Samplers.probabilitySampler(0.0001);
+
+ StackdriverExporterWorker(
+ String projectId,
+ MetricServiceClient metricServiceClient,
+ Duration exportInterval,
+ ViewManager viewManager,
+ MonitoredResource monitoredResource,
+ @javax.annotation.Nullable String metricNamePrefix) {
+ this.scheduleDelayMillis = exportInterval.toMillis();
+ this.projectId = projectId;
+ projectName = ProjectName.newBuilder().setProject(projectId).build();
+ this.metricServiceClient = metricServiceClient;
+ this.viewManager = viewManager;
+ this.monitoredResource = monitoredResource;
+ this.domain = getDomain(metricNamePrefix);
+ this.displayNamePrefix = getDisplayNamePrefix(metricNamePrefix);
+
+ Tracing.getExportComponent()
+ .getSampledSpanStore()
+ .registerSpanNamesForCollection(
+ Collections.singletonList("ExportStatsToStackdriverMonitoring"));
+ }
+
+ // Returns true if the given view is successfully registered to Stackdriver Monitoring, or the
+ // exact same view has already been registered. Returns false otherwise.
+ @VisibleForTesting
+ boolean registerView(View view) {
+ View existing = registeredViews.get(view.getName());
+ if (existing != null) {
+ if (existing.equals(view)) {
+ // Ignore views that are already registered.
+ return true;
+ } else {
+ // If we upload a view that has the same name with a registered view but with different
+ // attributes, Stackdriver client will throw an exception.
+ logger.log(
+ Level.WARNING,
+ "A different view with the same name is already registered: " + existing);
+ return false;
+ }
+ }
+ registeredViews.put(view.getName(), view);
+
+ Span span = tracer.getCurrentSpan();
+ span.addAnnotation("Create Stackdriver Metric.");
+ // TODO(songya): don't need to create MetricDescriptor for RpcViewConstants once we defined
+ // canonical metrics. Registration is required only for custom view definitions. Canonical
+ // views should be pre-registered.
+ MetricDescriptor metricDescriptor =
+ StackdriverExportUtils.createMetricDescriptor(view, projectId, domain, displayNamePrefix);
+ if (metricDescriptor == null) {
+ // Don't register interval views in this version.
+ return false;
+ }
+
+ CreateMetricDescriptorRequest request =
+ CreateMetricDescriptorRequest.newBuilder()
+ .setName(projectName.toString())
+ .setMetricDescriptor(metricDescriptor)
+ .build();
+ try {
+ metricServiceClient.createMetricDescriptor(request);
+ span.addAnnotation("Finish creating MetricDescriptor.");
+ return true;
+ } catch (ApiException e) {
+ logger.log(Level.WARNING, "ApiException thrown when creating MetricDescriptor.", e);
+ span.setStatus(
+ Status.CanonicalCode.valueOf(e.getStatusCode().getCode().name())
+ .toStatus()
+ .withDescription(
+ "ApiException thrown when creating MetricDescriptor: " + exceptionMessage(e)));
+ return false;
+ } catch (Throwable e) {
+ logger.log(Level.WARNING, "Exception thrown when creating MetricDescriptor.", e);
+ span.setStatus(
+ Status.UNKNOWN.withDescription(
+ "Exception thrown when creating MetricDescriptor: " + exceptionMessage(e)));
+ return false;
+ }
+ }
+
+ // Polls ViewData from Stats library for all exported views, and upload them as TimeSeries to
+ // StackDriver.
+ @VisibleForTesting
+ void export() {
+ List</*@Nullable*/ ViewData> viewDataList = Lists.newArrayList();
+ for (View view : viewManager.getAllExportedViews()) {
+ if (registerView(view)) {
+ // Only upload stats for valid views.
+ viewDataList.add(viewManager.getView(view.getName()));
+ }
+ }
+
+ List<TimeSeries> timeSeriesList = Lists.newArrayList();
+ for (/*@Nullable*/ ViewData viewData : viewDataList) {
+ timeSeriesList.addAll(
+ StackdriverExportUtils.createTimeSeriesList(viewData, monitoredResource, domain));
+ }
+ for (List<TimeSeries> batchedTimeSeries :
+ Lists.partition(timeSeriesList, MAX_BATCH_EXPORT_SIZE)) {
+ Span span = tracer.getCurrentSpan();
+ span.addAnnotation("Export Stackdriver TimeSeries.");
+ try {
+ CreateTimeSeriesRequest request =
+ CreateTimeSeriesRequest.newBuilder()
+ .setName(projectName.toString())
+ .addAllTimeSeries(batchedTimeSeries)
+ .build();
+ metricServiceClient.createTimeSeries(request);
+ span.addAnnotation("Finish exporting TimeSeries.");
+ } catch (ApiException e) {
+ logger.log(Level.WARNING, "ApiException thrown when exporting TimeSeries.", e);
+ span.setStatus(
+ Status.CanonicalCode.valueOf(e.getStatusCode().getCode().name())
+ .toStatus()
+ .withDescription(
+ "ApiException thrown when exporting TimeSeries: " + exceptionMessage(e)));
+ } catch (Throwable e) {
+ logger.log(Level.WARNING, "Exception thrown when exporting TimeSeries.", e);
+ span.setStatus(
+ Status.UNKNOWN.withDescription(
+ "Exception thrown when exporting TimeSeries: " + exceptionMessage(e)));
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ Span span =
+ tracer
+ .spanBuilder("ExportStatsToStackdriverMonitoring")
+ .setRecordEvents(true)
+ .setSampler(probabilitySampler)
+ .startSpan();
+ Scope scope = tracer.withSpan(span);
+ try {
+ export();
+ } catch (Throwable e) {
+ logger.log(Level.WARNING, "Exception thrown by the Stackdriver stats exporter.", e);
+ span.setStatus(
+ Status.UNKNOWN.withDescription(
+ "Exception from Stackdriver Exporter: " + exceptionMessage(e)));
+ } finally {
+ scope.close();
+ span.end();
+ }
+ try {
+ Thread.sleep(scheduleDelayMillis);
+ } catch (InterruptedException ie) {
+ // Preserve the interruption status as per guidance and stop doing any work.
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ }
+
+ private static String exceptionMessage(Throwable e) {
+ return e.getMessage() != null ? e.getMessage() : e.getClass().getName();
+ }
+
+ @VisibleForTesting
+ static String getDomain(@javax.annotation.Nullable String metricNamePrefix) {
+ String domain;
+ if (Strings.isNullOrEmpty(metricNamePrefix)) {
+ domain = CUSTOM_OPENCENSUS_DOMAIN;
+ } else {
+ if (!metricNamePrefix.endsWith("/")) {
+ domain = metricNamePrefix + '/';
+ } else {
+ domain = metricNamePrefix;
+ }
+ }
+ return domain;
+ }
+
+ @VisibleForTesting
+ static String getDisplayNamePrefix(@javax.annotation.Nullable String metricNamePrefix) {
+ if (metricNamePrefix == null) {
+ return DEFAULT_DISPLAY_NAME_PREFIX;
+ } else {
+ if (!metricNamePrefix.endsWith("/") && !metricNamePrefix.isEmpty()) {
+ metricNamePrefix += '/';
+ }
+ return metricNamePrefix;
+ }
+ }
+}
diff --git a/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfiguration.java b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfiguration.java
new file mode 100644
index 00000000..c4008ca1
--- /dev/null
+++ b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfiguration.java
@@ -0,0 +1,159 @@
+/*
+ * 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.stats.stackdriver;
+
+import com.google.api.MonitoredResource;
+import com.google.auth.Credentials;
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Duration;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Configurations for {@link StackdriverStatsExporter}.
+ *
+ * @since 0.11
+ */
+@AutoValue
+@Immutable
+public abstract class StackdriverStatsConfiguration {
+
+ StackdriverStatsConfiguration() {}
+
+ /**
+ * Returns the {@link Credentials}.
+ *
+ * @return the {@code Credentials}.
+ * @since 0.11
+ */
+ @Nullable
+ public abstract Credentials getCredentials();
+
+ /**
+ * Returns the project id.
+ *
+ * @return the project id.
+ * @since 0.11
+ */
+ @Nullable
+ public abstract String getProjectId();
+
+ /**
+ * Returns the export interval between pushes to StackDriver.
+ *
+ * @return the export interval.
+ * @since 0.11
+ */
+ @Nullable
+ public abstract Duration getExportInterval();
+
+ /**
+ * Returns the Stackdriver {@link MonitoredResource}.
+ *
+ * @return the {@code MonitoredResource}.
+ * @since 0.11
+ */
+ @Nullable
+ public abstract MonitoredResource getMonitoredResource();
+
+ /**
+ * Returns the name prefix for Stackdriver metrics.
+ *
+ * @return the metric name prefix.
+ * @since 0.16
+ */
+ @Nullable
+ public abstract String getMetricNamePrefix();
+
+ /**
+ * Returns a new {@link Builder}.
+ *
+ * @return a {@code Builder}.
+ * @since 0.11
+ */
+ public static Builder builder() {
+ return new AutoValue_StackdriverStatsConfiguration.Builder();
+ }
+
+ /**
+ * Builder for {@link StackdriverStatsConfiguration}.
+ *
+ * @since 0.11
+ */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ Builder() {}
+
+ /**
+ * Sets the given {@link Credentials}.
+ *
+ * @param credentials the {@code Credentials}.
+ * @return this.
+ * @since 0.11
+ */
+ public abstract Builder setCredentials(Credentials credentials);
+
+ /**
+ * Sets the given project id.
+ *
+ * @param projectId the cloud project id.
+ * @return this.
+ * @since 0.11
+ */
+ public abstract Builder setProjectId(String projectId);
+
+ /**
+ * Sets the export interval.
+ *
+ * @param exportInterval the export interval between pushes to StackDriver.
+ * @return this.
+ * @since 0.11
+ */
+ public abstract Builder setExportInterval(Duration exportInterval);
+
+ /**
+ * Sets the {@link MonitoredResource}.
+ *
+ * @param monitoredResource the Stackdriver {@code MonitoredResource}.
+ * @return this.
+ * @since 0.11
+ */
+ public abstract Builder setMonitoredResource(MonitoredResource monitoredResource);
+
+ /**
+ * Sets the the name prefix for Stackdriver metrics.
+ *
+ * <p>It is suggested to use prefix with custom or external domain name, for example
+ * "custom.googleapis.com/myorg/" or "external.googleapis.com/prometheus/". If the given prefix
+ * doesn't start with a valid domain, we will add "custom.googleapis.com/" before the prefix.
+ *
+ * @param prefix the metric name prefix.
+ * @return this.
+ * @since 0.16
+ */
+ public abstract Builder setMetricNamePrefix(String prefix);
+
+ /**
+ * Builds a new {@link StackdriverStatsConfiguration} with current settings.
+ *
+ * @return a {@code StackdriverStatsConfiguration}.
+ * @since 0.11
+ */
+ public abstract StackdriverStatsConfiguration build();
+ }
+}
diff --git a/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporter.java b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporter.java
new file mode 100644
index 00000000..51c54916
--- /dev/null
+++ b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporter.java
@@ -0,0 +1,363 @@
+/*
+ * 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.stats.stackdriver;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.api.MonitoredResource;
+import com.google.api.gax.core.FixedCredentialsProvider;
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.cloud.ServiceOptions;
+import com.google.cloud.monitoring.v3.MetricServiceClient;
+import com.google.cloud.monitoring.v3.MetricServiceSettings;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.MoreExecutors;
+import io.opencensus.common.Duration;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.ViewManager;
+import java.io.IOException;
+import java.util.concurrent.ThreadFactory;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Exporter to Stackdriver Monitoring Client API v3.
+ *
+ * <p>Example of usage on Google Cloud VMs:
+ *
+ * <pre><code>
+ * public static void main(String[] args) {
+ * StackdriverStatsExporter.createAndRegister(
+ * StackdriverStatsConfiguration
+ * .builder()
+ * .setProjectId("MyStackdriverProjectId")
+ * .setExportInterval(Duration.fromMillis(100000))
+ * .build());
+ * ... // Do work.
+ * }
+ * </code></pre>
+ *
+ * @since 0.9
+ */
+public final class StackdriverStatsExporter {
+
+ private static final Object monitor = new Object();
+
+ private final Thread workerThread;
+
+ @GuardedBy("monitor")
+ @Nullable
+ private static StackdriverStatsExporter exporter = null;
+
+ private static final Duration ZERO = Duration.create(0, 0);
+
+ @VisibleForTesting static final Duration DEFAULT_INTERVAL = Duration.create(60, 0);
+
+ private static final MonitoredResource DEFAULT_RESOURCE =
+ StackdriverExportUtils.getDefaultResource();
+
+ @VisibleForTesting
+ StackdriverStatsExporter(
+ String projectId,
+ MetricServiceClient metricServiceClient,
+ Duration exportInterval,
+ ViewManager viewManager,
+ MonitoredResource monitoredResource,
+ @Nullable String metricNamePrefix) {
+ checkArgument(exportInterval.compareTo(ZERO) > 0, "Duration must be positive");
+ StackdriverExporterWorker worker =
+ new StackdriverExporterWorker(
+ projectId,
+ metricServiceClient,
+ exportInterval,
+ viewManager,
+ monitoredResource,
+ metricNamePrefix);
+ this.workerThread = new DaemonThreadFactory().newThread(worker);
+ }
+
+ /**
+ * Creates a StackdriverStatsExporter for an explicit project ID and using explicit credentials,
+ * with default Monitored Resource.
+ *
+ * <p>Only one Stackdriver exporter can be created.
+ *
+ * @param credentials a credentials used to authenticate API calls.
+ * @param projectId the cloud project id.
+ * @param exportInterval the interval between pushing stats to StackDriver.
+ * @throws IllegalStateException if a Stackdriver exporter already exists.
+ * @deprecated in favor of {@link #createAndRegister(StackdriverStatsConfiguration)}.
+ * @since 0.9
+ */
+ @Deprecated
+ public static void createAndRegisterWithCredentialsAndProjectId(
+ Credentials credentials, String projectId, Duration exportInterval) throws IOException {
+ checkNotNull(credentials, "credentials");
+ checkNotNull(projectId, "projectId");
+ checkNotNull(exportInterval, "exportInterval");
+ createInternal(credentials, projectId, exportInterval, null, null);
+ }
+
+ /**
+ * Creates a Stackdriver Stats exporter for an explicit project ID, with default Monitored
+ * Resource.
+ *
+ * <p>Only one Stackdriver exporter can be created.
+ *
+ * <p>This uses the default application credentials. See {@link
+ * GoogleCredentials#getApplicationDefault}.
+ *
+ * <p>This is equivalent with:
+ *
+ * <pre>{@code
+ * StackdriverStatsExporter.createWithCredentialsAndProjectId(
+ * GoogleCredentials.getApplicationDefault(), projectId);
+ * }</pre>
+ *
+ * @param projectId the cloud project id.
+ * @param exportInterval the interval between pushing stats to StackDriver.
+ * @throws IllegalStateException if a Stackdriver exporter is already created.
+ * @deprecated in favor of {@link #createAndRegister(StackdriverStatsConfiguration)}.
+ * @since 0.9
+ */
+ @Deprecated
+ public static void createAndRegisterWithProjectId(String projectId, Duration exportInterval)
+ throws IOException {
+ checkNotNull(projectId, "projectId");
+ checkNotNull(exportInterval, "exportInterval");
+ createInternal(null, projectId, exportInterval, null, null);
+ }
+
+ /**
+ * Creates a Stackdriver Stats exporter with a {@link StackdriverStatsConfiguration}.
+ *
+ * <p>Only one Stackdriver exporter can be created.
+ *
+ * <p>If {@code credentials} of the configuration is not set, the exporter will use the default
+ * application credentials. See {@link GoogleCredentials#getApplicationDefault}.
+ *
+ * <p>If {@code projectId} of the configuration is not set, the exporter will use the default
+ * project ID configured. See {@link ServiceOptions#getDefaultProjectId}.
+ *
+ * <p>If {@code exportInterval} of the configuration is not set, the exporter will use the default
+ * interval of one minute.
+ *
+ * <p>If {@code monitoredResources} of the configuration is not set, the exporter will try to
+ * create an appropriate {@code monitoredResources} based on the environment variables. In
+ * addition, please refer to
+ * cloud.google.com/monitoring/custom-metrics/creating-metrics#which-resource for a list of valid
+ * {@code MonitoredResource}s.
+ *
+ * <p>If {@code metricNamePrefix} of the configuration is not set, the exporter will use the
+ * default prefix "OpenCensus".
+ *
+ * @param configuration the {@code StackdriverStatsConfiguration}.
+ * @throws IllegalStateException if a Stackdriver exporter is already created.
+ * @since 0.11.0
+ */
+ public static void createAndRegister(StackdriverStatsConfiguration configuration)
+ throws IOException {
+ checkNotNull(configuration, "configuration");
+ createInternal(
+ configuration.getCredentials(),
+ configuration.getProjectId(),
+ configuration.getExportInterval(),
+ configuration.getMonitoredResource(),
+ configuration.getMetricNamePrefix());
+ }
+
+ /**
+ * Creates a Stackdriver Stats exporter with default settings.
+ *
+ * <p>Only one Stackdriver exporter can be created.
+ *
+ * <p>This is equivalent with:
+ *
+ * <pre>{@code
+ * StackdriverStatsExporter.createAndRegister(StackdriverStatsConfiguration.builder().build());
+ * }</pre>
+ *
+ * <p>This method uses the default application credentials. See {@link
+ * GoogleCredentials#getApplicationDefault}.
+ *
+ * <p>This method uses the default project ID configured. See {@link
+ * ServiceOptions#getDefaultProjectId}.
+ *
+ * <p>This method uses the default interval of one minute.
+ *
+ * <p>This method uses the default resource created from the environment variables.
+ *
+ * <p>This method uses the default display name prefix "OpenCensus".
+ *
+ * @throws IllegalStateException if a Stackdriver exporter is already created.
+ * @since 0.11.0
+ */
+ public static void createAndRegister() throws IOException {
+ createInternal(null, null, null, null, null);
+ }
+
+ /**
+ * Creates a Stackdriver Stats exporter with default Monitored Resource.
+ *
+ * <p>Only one Stackdriver exporter can be created.
+ *
+ * <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
+ * StackdriverStatsExporter.createWithProjectId(ServiceOptions.getDefaultProjectId());
+ * }</pre>
+ *
+ * @param exportInterval the interval between pushing stats to StackDriver.
+ * @throws IllegalStateException if a Stackdriver exporter is already created.
+ * @deprecated in favor of {@link #createAndRegister(StackdriverStatsConfiguration)}.
+ * @since 0.9
+ */
+ @Deprecated
+ public static void createAndRegister(Duration exportInterval) throws IOException {
+ checkNotNull(exportInterval, "exportInterval");
+ createInternal(null, null, exportInterval, null, null);
+ }
+
+ /**
+ * Creates a Stackdriver Stats exporter with an explicit project ID and a custom Monitored
+ * Resource.
+ *
+ * <p>Only one Stackdriver exporter can be created.
+ *
+ * <p>Please refer to cloud.google.com/monitoring/custom-metrics/creating-metrics#which-resource
+ * for a list of valid {@code MonitoredResource}s.
+ *
+ * <p>This uses the default application credentials. See {@link
+ * GoogleCredentials#getApplicationDefault}.
+ *
+ * @param projectId the cloud project id.
+ * @param exportInterval the interval between pushing stats to StackDriver.
+ * @param monitoredResource the Monitored Resource used by exporter.
+ * @throws IllegalStateException if a Stackdriver exporter is already created.
+ * @deprecated in favor of {@link #createAndRegister(StackdriverStatsConfiguration)}.
+ * @since 0.10
+ */
+ @Deprecated
+ public static void createAndRegisterWithProjectIdAndMonitoredResource(
+ String projectId, Duration exportInterval, MonitoredResource monitoredResource)
+ throws IOException {
+ checkNotNull(projectId, "projectId");
+ checkNotNull(exportInterval, "exportInterval");
+ checkNotNull(monitoredResource, "monitoredResource");
+ createInternal(null, projectId, exportInterval, monitoredResource, null);
+ }
+
+ /**
+ * Creates a Stackdriver Stats exporter with a custom Monitored Resource.
+ *
+ * <p>Only one Stackdriver exporter can be created.
+ *
+ * <p>Please refer to cloud.google.com/monitoring/custom-metrics/creating-metrics#which-resource
+ * for a list of valid {@code MonitoredResource}s.
+ *
+ * <p>This uses the default application credentials. See {@link
+ * GoogleCredentials#getApplicationDefault}.
+ *
+ * <p>This uses the default project ID configured see {@link ServiceOptions#getDefaultProjectId}.
+ *
+ * @param exportInterval the interval between pushing stats to StackDriver.
+ * @param monitoredResource the Monitored Resource used by exporter.
+ * @throws IllegalStateException if a Stackdriver exporter is already created.
+ * @deprecated in favor of {@link #createAndRegister(StackdriverStatsConfiguration)}.
+ * @since 0.10
+ */
+ @Deprecated
+ public static void createAndRegisterWithMonitoredResource(
+ Duration exportInterval, MonitoredResource monitoredResource) throws IOException {
+ checkNotNull(exportInterval, "exportInterval");
+ checkNotNull(monitoredResource, "monitoredResource");
+ createInternal(null, null, exportInterval, monitoredResource, null);
+ }
+
+ // Use createInternal() (instead of constructor) to enforce singleton.
+ private static void createInternal(
+ @Nullable Credentials credentials,
+ @Nullable String projectId,
+ @Nullable Duration exportInterval,
+ @Nullable MonitoredResource monitoredResource,
+ @Nullable String metricNamePrefix)
+ throws IOException {
+ projectId = projectId == null ? ServiceOptions.getDefaultProjectId() : projectId;
+ exportInterval = exportInterval == null ? DEFAULT_INTERVAL : exportInterval;
+ monitoredResource = monitoredResource == null ? DEFAULT_RESOURCE : monitoredResource;
+ synchronized (monitor) {
+ checkState(exporter == null, "Stackdriver stats exporter is already created.");
+ MetricServiceClient metricServiceClient;
+ // Initialize MetricServiceClient inside lock to avoid creating multiple clients.
+ if (credentials == null) {
+ metricServiceClient = MetricServiceClient.create();
+ } else {
+ metricServiceClient =
+ MetricServiceClient.create(
+ MetricServiceSettings.newBuilder()
+ .setCredentialsProvider(FixedCredentialsProvider.create(credentials))
+ .build());
+ }
+ exporter =
+ new StackdriverStatsExporter(
+ projectId,
+ metricServiceClient,
+ exportInterval,
+ Stats.getViewManager(),
+ monitoredResource,
+ metricNamePrefix);
+ exporter.workerThread.start();
+ }
+ }
+
+ // Resets exporter to null. Used only for unit tests.
+ @VisibleForTesting
+ static void unsafeResetExporter() {
+ synchronized (monitor) {
+ StackdriverStatsExporter.exporter = null;
+ }
+ }
+
+ /** A lightweight {@link ThreadFactory} to spawn threads in a GAE-Java7-compatible way. */
+ // TODO(Hailong): Remove this once we use a callback to implement the exporter.
+ static final class DaemonThreadFactory implements ThreadFactory {
+ // AppEngine runtimes have constraints on threading and socket handling
+ // that need to be accommodated.
+ public static final boolean IS_RESTRICTED_APPENGINE =
+ System.getProperty("com.google.appengine.runtime.environment") != null
+ && "1.7".equals(System.getProperty("java.specification.version"));
+ private static final ThreadFactory threadFactory = MoreExecutors.platformThreadFactory();
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread thread = threadFactory.newThread(r);
+ if (!IS_RESTRICTED_APPENGINE) {
+ thread.setName("ExportWorkerThread");
+ thread.setDaemon(true);
+ }
+ return thread;
+ }
+ }
+}
diff --git a/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtilsTest.java b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtilsTest.java
new file mode 100644
index 00000000..cd536e8f
--- /dev/null
+++ b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtilsTest.java
@@ -0,0 +1,568 @@
+/*
+ * 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.stats.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.stats.stackdriver.StackdriverExporterWorker.CUSTOM_OPENCENSUS_DOMAIN;
+import static io.opencensus.exporter.stats.stackdriver.StackdriverExporterWorker.DEFAULT_DISPLAY_NAME_PREFIX;
+
+import com.google.api.Distribution.BucketOptions;
+import com.google.api.Distribution.BucketOptions.Explicit;
+import com.google.api.LabelDescriptor;
+import com.google.api.LabelDescriptor.ValueType;
+import com.google.api.Metric;
+import com.google.api.MetricDescriptor;
+import com.google.api.MetricDescriptor.MetricKind;
+import com.google.api.MonitoredResource;
+import com.google.common.collect.ImmutableMap;
+import com.google.monitoring.v3.Point;
+import com.google.monitoring.v3.TimeInterval;
+import com.google.monitoring.v3.TimeSeries;
+import com.google.monitoring.v3.TypedValue;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import io.opencensus.stats.View.Name;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewData.AggregationWindowData.IntervalData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.lang.management.ManagementFactory;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link StackdriverExportUtils}. */
+@RunWith(JUnit4.class)
+public class StackdriverExportUtilsTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ private static final TagKey KEY = TagKey.create("KEY");
+ private static final TagKey KEY_2 = TagKey.create("KEY2");
+ private static final TagKey KEY_3 = TagKey.create("KEY3");
+ private static final TagValue VALUE_1 = TagValue.create("VALUE1");
+ private static final TagValue VALUE_2 = TagValue.create("VALUE2");
+ private static final String MEASURE_UNIT = "us";
+ private static final String MEASURE_DESCRIPTION = "measure description";
+ private static final MeasureDouble MEASURE_DOUBLE =
+ MeasureDouble.create("measure1", MEASURE_DESCRIPTION, MEASURE_UNIT);
+ private static final MeasureLong MEASURE_LONG =
+ MeasureLong.create("measure2", MEASURE_DESCRIPTION, MEASURE_UNIT);
+ private static final String VIEW_NAME = "view";
+ private static final String VIEW_DESCRIPTION = "view description";
+ private static final Duration TEN_SECONDS = Duration.create(10, 0);
+ private static final Cumulative CUMULATIVE = Cumulative.create();
+ private static final Interval INTERVAL = Interval.create(TEN_SECONDS);
+ private static final BucketBoundaries BUCKET_BOUNDARIES =
+ BucketBoundaries.create(Arrays.asList(0.0, 1.0, 3.0, 5.0));
+ private static final Sum SUM = Sum.create();
+ private static final Count COUNT = Count.create();
+ private static final Mean MEAN = Mean.create();
+ private static final Distribution DISTRIBUTION = Distribution.create(BUCKET_BOUNDARIES);
+ private static final LastValue LAST_VALUE = LastValue.create();
+ private static final String PROJECT_ID = "id";
+ private static final MonitoredResource DEFAULT_RESOURCE =
+ MonitoredResource.newBuilder().setType("global").build();
+ private static final String DEFAULT_TASK_VALUE =
+ "java-" + ManagementFactory.getRuntimeMXBean().getName();
+
+ @Test
+ public void testConstant() {
+ assertThat(StackdriverExportUtils.LABEL_DESCRIPTION).isEqualTo("OpenCensus TagKey");
+ }
+
+ @Test
+ public void createLabelDescriptor() {
+ assertThat(StackdriverExportUtils.createLabelDescriptor(TagKey.create("string")))
+ .isEqualTo(
+ LabelDescriptor.newBuilder()
+ .setKey("string")
+ .setDescription(StackdriverExportUtils.LABEL_DESCRIPTION)
+ .setValueType(ValueType.STRING)
+ .build());
+ }
+
+ @Test
+ public void createMetricKind() {
+ assertThat(StackdriverExportUtils.createMetricKind(CUMULATIVE, SUM))
+ .isEqualTo(MetricKind.CUMULATIVE);
+ assertThat(StackdriverExportUtils.createMetricKind(INTERVAL, COUNT))
+ .isEqualTo(MetricKind.UNRECOGNIZED);
+ assertThat(StackdriverExportUtils.createMetricKind(CUMULATIVE, LAST_VALUE))
+ .isEqualTo(MetricKind.GAUGE);
+ assertThat(StackdriverExportUtils.createMetricKind(INTERVAL, LAST_VALUE))
+ .isEqualTo(MetricKind.GAUGE);
+ }
+
+ @Test
+ public void createValueType() {
+ assertThat(StackdriverExportUtils.createValueType(SUM, MEASURE_DOUBLE))
+ .isEqualTo(MetricDescriptor.ValueType.DOUBLE);
+ assertThat(StackdriverExportUtils.createValueType(SUM, MEASURE_LONG))
+ .isEqualTo(MetricDescriptor.ValueType.INT64);
+ assertThat(StackdriverExportUtils.createValueType(COUNT, MEASURE_DOUBLE))
+ .isEqualTo(MetricDescriptor.ValueType.INT64);
+ assertThat(StackdriverExportUtils.createValueType(COUNT, MEASURE_LONG))
+ .isEqualTo(MetricDescriptor.ValueType.INT64);
+ assertThat(StackdriverExportUtils.createValueType(MEAN, MEASURE_DOUBLE))
+ .isEqualTo(MetricDescriptor.ValueType.DOUBLE);
+ assertThat(StackdriverExportUtils.createValueType(MEAN, MEASURE_LONG))
+ .isEqualTo(MetricDescriptor.ValueType.DOUBLE);
+ assertThat(StackdriverExportUtils.createValueType(DISTRIBUTION, MEASURE_DOUBLE))
+ .isEqualTo(MetricDescriptor.ValueType.DISTRIBUTION);
+ assertThat(StackdriverExportUtils.createValueType(DISTRIBUTION, MEASURE_LONG))
+ .isEqualTo(MetricDescriptor.ValueType.DISTRIBUTION);
+ assertThat(StackdriverExportUtils.createValueType(LAST_VALUE, MEASURE_DOUBLE))
+ .isEqualTo(MetricDescriptor.ValueType.DOUBLE);
+ assertThat(StackdriverExportUtils.createValueType(LAST_VALUE, MEASURE_LONG))
+ .isEqualTo(MetricDescriptor.ValueType.INT64);
+ }
+
+ @Test
+ public void createUnit() {
+ assertThat(StackdriverExportUtils.createUnit(SUM, MEASURE_DOUBLE)).isEqualTo(MEASURE_UNIT);
+ assertThat(StackdriverExportUtils.createUnit(COUNT, MEASURE_DOUBLE)).isEqualTo("1");
+ assertThat(StackdriverExportUtils.createUnit(MEAN, MEASURE_DOUBLE)).isEqualTo(MEASURE_UNIT);
+ assertThat(StackdriverExportUtils.createUnit(DISTRIBUTION, MEASURE_DOUBLE))
+ .isEqualTo(MEASURE_UNIT);
+ assertThat(StackdriverExportUtils.createUnit(LAST_VALUE, MEASURE_DOUBLE))
+ .isEqualTo(MEASURE_UNIT);
+ }
+
+ @Test
+ public void createMetric() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ assertThat(
+ StackdriverExportUtils.createMetric(
+ view, Arrays.asList(VALUE_1), CUSTOM_OPENCENSUS_DOMAIN))
+ .isEqualTo(
+ Metric.newBuilder()
+ .setType("custom.googleapis.com/opencensus/" + VIEW_NAME)
+ .putLabels("KEY", "VALUE1")
+ .putLabels(StackdriverExportUtils.OPENCENSUS_TASK, DEFAULT_TASK_VALUE)
+ .build());
+ }
+
+ @Test
+ public void createMetric_WithExternalMetricDomain() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ String prometheusDomain = "external.googleapis.com/prometheus/";
+ assertThat(StackdriverExportUtils.createMetric(view, Arrays.asList(VALUE_1), prometheusDomain))
+ .isEqualTo(
+ Metric.newBuilder()
+ .setType(prometheusDomain + VIEW_NAME)
+ .putLabels("KEY", "VALUE1")
+ .putLabels(StackdriverExportUtils.OPENCENSUS_TASK, DEFAULT_TASK_VALUE)
+ .build());
+ }
+
+ @Test
+ public void createMetric_skipNullTagValue() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY, KEY_2, KEY_3),
+ CUMULATIVE);
+ assertThat(
+ StackdriverExportUtils.createMetric(
+ view, Arrays.asList(VALUE_1, null, VALUE_2), CUSTOM_OPENCENSUS_DOMAIN))
+ .isEqualTo(
+ Metric.newBuilder()
+ .setType("custom.googleapis.com/opencensus/" + VIEW_NAME)
+ .putLabels("KEY", "VALUE1")
+ .putLabels("KEY3", "VALUE2")
+ .putLabels(StackdriverExportUtils.OPENCENSUS_TASK, DEFAULT_TASK_VALUE)
+ .build());
+ }
+
+ @Test
+ public void createMetric_throwWhenTagKeysAndValuesHaveDifferentSize() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY, KEY_2, KEY_3),
+ CUMULATIVE);
+ List<TagValue> tagValues = Arrays.asList(VALUE_1, null);
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("TagKeys and TagValues don't have same size.");
+ StackdriverExportUtils.createMetric(view, tagValues, CUSTOM_OPENCENSUS_DOMAIN);
+ }
+
+ @Test
+ public void convertTimestamp() {
+ Timestamp censusTimestamp1 = Timestamp.create(100, 3000);
+ assertThat(StackdriverExportUtils.convertTimestamp(censusTimestamp1))
+ .isEqualTo(
+ com.google.protobuf.Timestamp.newBuilder().setSeconds(100).setNanos(3000).build());
+
+ // Stackdriver doesn't allow negative values, instead it will replace the negative values
+ // by returning a default instance.
+ Timestamp censusTimestamp2 = Timestamp.create(-100, 3000);
+ assertThat(StackdriverExportUtils.convertTimestamp(censusTimestamp2))
+ .isEqualTo(com.google.protobuf.Timestamp.newBuilder().build());
+ }
+
+ @Test
+ public void createTimeInterval_cumulative() {
+ Timestamp censusTimestamp1 = Timestamp.create(100, 3000);
+ Timestamp censusTimestamp2 = Timestamp.create(200, 0);
+ assertThat(
+ StackdriverExportUtils.createTimeInterval(
+ CumulativeData.create(censusTimestamp1, censusTimestamp2), DISTRIBUTION))
+ .isEqualTo(
+ TimeInterval.newBuilder()
+ .setStartTime(StackdriverExportUtils.convertTimestamp(censusTimestamp1))
+ .setEndTime(StackdriverExportUtils.convertTimestamp(censusTimestamp2))
+ .build());
+ assertThat(
+ StackdriverExportUtils.createTimeInterval(
+ CumulativeData.create(censusTimestamp1, censusTimestamp2), LAST_VALUE))
+ .isEqualTo(
+ TimeInterval.newBuilder()
+ .setEndTime(StackdriverExportUtils.convertTimestamp(censusTimestamp2))
+ .build());
+ }
+
+ @Test
+ public void createTimeInterval_interval() {
+ IntervalData intervalData = IntervalData.create(Timestamp.create(200, 0));
+ // Only Cumulative view will supported in this version.
+ thrown.expect(IllegalArgumentException.class);
+ StackdriverExportUtils.createTimeInterval(intervalData, SUM);
+ }
+
+ @Test
+ public void createBucketOptions() {
+ assertThat(StackdriverExportUtils.createBucketOptions(BUCKET_BOUNDARIES))
+ .isEqualTo(
+ BucketOptions.newBuilder()
+ .setExplicitBuckets(
+ Explicit.newBuilder().addAllBounds(Arrays.asList(0.0, 1.0, 3.0, 5.0)))
+ .build());
+ }
+
+ @Test
+ public void createDistribution() {
+ DistributionData distributionData =
+ DistributionData.create(2, 3, 0, 5, 14, Arrays.asList(0L, 1L, 1L, 0L, 1L));
+ assertThat(StackdriverExportUtils.createDistribution(distributionData, BUCKET_BOUNDARIES))
+ .isEqualTo(
+ com.google.api.Distribution.newBuilder()
+ .setMean(2)
+ .setCount(3)
+ // TODO(songya): uncomment this once Stackdriver supports setting max and min.
+ // .setRange(
+ // com.google.api.Distribution.Range.newBuilder().setMin(0).setMax(5).build())
+ .setBucketOptions(StackdriverExportUtils.createBucketOptions(BUCKET_BOUNDARIES))
+ .addAllBucketCounts(Arrays.asList(0L, 1L, 1L, 0L, 1L))
+ .setSumOfSquaredDeviation(14)
+ .build());
+ }
+
+ @Test
+ public void createTypedValue() {
+ assertThat(StackdriverExportUtils.createTypedValue(SUM, SumDataDouble.create(1.1)))
+ .isEqualTo(TypedValue.newBuilder().setDoubleValue(1.1).build());
+ assertThat(StackdriverExportUtils.createTypedValue(SUM, SumDataLong.create(10000)))
+ .isEqualTo(TypedValue.newBuilder().setInt64Value(10000).build());
+ assertThat(StackdriverExportUtils.createTypedValue(COUNT, CountData.create(55)))
+ .isEqualTo(TypedValue.newBuilder().setInt64Value(55).build());
+ assertThat(StackdriverExportUtils.createTypedValue(MEAN, MeanData.create(7.7, 8)))
+ .isEqualTo(TypedValue.newBuilder().setDoubleValue(7.7).build());
+ DistributionData distributionData =
+ DistributionData.create(2, 3, 0, 5, 14, Arrays.asList(0L, 1L, 1L, 0L, 1L));
+ assertThat(StackdriverExportUtils.createTypedValue(DISTRIBUTION, distributionData))
+ .isEqualTo(
+ TypedValue.newBuilder()
+ .setDistributionValue(
+ StackdriverExportUtils.createDistribution(distributionData, BUCKET_BOUNDARIES))
+ .build());
+ assertThat(StackdriverExportUtils.createTypedValue(LAST_VALUE, LastValueDataDouble.create(9.9)))
+ .isEqualTo(TypedValue.newBuilder().setDoubleValue(9.9).build());
+ assertThat(StackdriverExportUtils.createTypedValue(LAST_VALUE, LastValueDataLong.create(90000)))
+ .isEqualTo(TypedValue.newBuilder().setInt64Value(90000).build());
+ }
+
+ @Test
+ public void createPoint_cumulative() {
+ Timestamp censusTimestamp1 = Timestamp.create(100, 3000);
+ Timestamp censusTimestamp2 = Timestamp.create(200, 0);
+ CumulativeData cumulativeData = CumulativeData.create(censusTimestamp1, censusTimestamp2);
+ SumDataDouble sumDataDouble = SumDataDouble.create(33.3);
+
+ assertThat(StackdriverExportUtils.createPoint(sumDataDouble, cumulativeData, SUM))
+ .isEqualTo(
+ Point.newBuilder()
+ .setInterval(StackdriverExportUtils.createTimeInterval(cumulativeData, SUM))
+ .setValue(StackdriverExportUtils.createTypedValue(SUM, sumDataDouble))
+ .build());
+ }
+
+ @Test
+ public void createPoint_interval() {
+ IntervalData intervalData = IntervalData.create(Timestamp.create(200, 0));
+ SumDataDouble sumDataDouble = SumDataDouble.create(33.3);
+ // Only Cumulative view will supported in this version.
+ thrown.expect(IllegalArgumentException.class);
+ StackdriverExportUtils.createPoint(sumDataDouble, intervalData, SUM);
+ }
+
+ @Test
+ public void createMetricDescriptor_cumulative() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ MetricDescriptor metricDescriptor =
+ StackdriverExportUtils.createMetricDescriptor(
+ view, PROJECT_ID, "custom.googleapis.com/myorg/", "myorg/");
+ assertThat(metricDescriptor.getName())
+ .isEqualTo(
+ "projects/"
+ + PROJECT_ID
+ + "/metricDescriptors/custom.googleapis.com/myorg/"
+ + VIEW_NAME);
+ assertThat(metricDescriptor.getDescription()).isEqualTo(VIEW_DESCRIPTION);
+ assertThat(metricDescriptor.getDisplayName()).isEqualTo("myorg/" + VIEW_NAME);
+ assertThat(metricDescriptor.getType()).isEqualTo("custom.googleapis.com/myorg/" + VIEW_NAME);
+ assertThat(metricDescriptor.getUnit()).isEqualTo(MEASURE_UNIT);
+ assertThat(metricDescriptor.getMetricKind()).isEqualTo(MetricKind.CUMULATIVE);
+ assertThat(metricDescriptor.getValueType()).isEqualTo(MetricDescriptor.ValueType.DISTRIBUTION);
+ assertThat(metricDescriptor.getLabelsList())
+ .containsExactly(
+ LabelDescriptor.newBuilder()
+ .setKey(KEY.getName())
+ .setDescription(StackdriverExportUtils.LABEL_DESCRIPTION)
+ .setValueType(ValueType.STRING)
+ .build(),
+ LabelDescriptor.newBuilder()
+ .setKey(StackdriverExportUtils.OPENCENSUS_TASK)
+ .setDescription(StackdriverExportUtils.OPENCENSUS_TASK_DESCRIPTION)
+ .setValueType(ValueType.STRING)
+ .build());
+ }
+
+ @Test
+ public void createMetricDescriptor_cumulative_count() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ COUNT,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ MetricDescriptor metricDescriptor =
+ StackdriverExportUtils.createMetricDescriptor(
+ view, PROJECT_ID, CUSTOM_OPENCENSUS_DOMAIN, DEFAULT_DISPLAY_NAME_PREFIX);
+ assertThat(metricDescriptor.getName())
+ .isEqualTo(
+ "projects/"
+ + PROJECT_ID
+ + "/metricDescriptors/custom.googleapis.com/opencensus/"
+ + VIEW_NAME);
+ assertThat(metricDescriptor.getDescription()).isEqualTo(VIEW_DESCRIPTION);
+ assertThat(metricDescriptor.getDisplayName()).isEqualTo("OpenCensus/" + VIEW_NAME);
+ assertThat(metricDescriptor.getType())
+ .isEqualTo("custom.googleapis.com/opencensus/" + VIEW_NAME);
+ assertThat(metricDescriptor.getUnit()).isEqualTo("1");
+ assertThat(metricDescriptor.getMetricKind()).isEqualTo(MetricKind.CUMULATIVE);
+ assertThat(metricDescriptor.getValueType()).isEqualTo(MetricDescriptor.ValueType.INT64);
+ assertThat(metricDescriptor.getLabelsList())
+ .containsExactly(
+ LabelDescriptor.newBuilder()
+ .setKey(KEY.getName())
+ .setDescription(StackdriverExportUtils.LABEL_DESCRIPTION)
+ .setValueType(ValueType.STRING)
+ .build(),
+ LabelDescriptor.newBuilder()
+ .setKey(StackdriverExportUtils.OPENCENSUS_TASK)
+ .setDescription(StackdriverExportUtils.OPENCENSUS_TASK_DESCRIPTION)
+ .setValueType(ValueType.STRING)
+ .build());
+ }
+
+ @Test
+ public void createMetricDescriptor_interval() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ INTERVAL);
+ assertThat(
+ StackdriverExportUtils.createMetricDescriptor(
+ view, PROJECT_ID, CUSTOM_OPENCENSUS_DOMAIN, DEFAULT_DISPLAY_NAME_PREFIX))
+ .isNull();
+ }
+
+ @Test
+ public void createTimeSeriesList_cumulative() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ DistributionData distributionData1 =
+ DistributionData.create(2, 3, 0, 5, 14, Arrays.asList(0L, 1L, 1L, 0L, 1L));
+ DistributionData distributionData2 =
+ DistributionData.create(-1, 1, -1, -1, 0, Arrays.asList(1L, 0L, 0L, 0L, 0L));
+ Map<List<TagValue>, DistributionData> aggregationMap =
+ ImmutableMap.of(
+ Arrays.asList(VALUE_1), distributionData1, Arrays.asList(VALUE_2), distributionData2);
+ CumulativeData cumulativeData =
+ CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000));
+ ViewData viewData = ViewData.create(view, aggregationMap, cumulativeData);
+ List<TimeSeries> timeSeriesList =
+ StackdriverExportUtils.createTimeSeriesList(
+ viewData, DEFAULT_RESOURCE, CUSTOM_OPENCENSUS_DOMAIN);
+ assertThat(timeSeriesList).hasSize(2);
+ TimeSeries expected1 =
+ TimeSeries.newBuilder()
+ .setMetricKind(MetricKind.CUMULATIVE)
+ .setValueType(MetricDescriptor.ValueType.DISTRIBUTION)
+ .setMetric(
+ StackdriverExportUtils.createMetric(
+ view, Arrays.asList(VALUE_1), CUSTOM_OPENCENSUS_DOMAIN))
+ .setResource(MonitoredResource.newBuilder().setType("global"))
+ .addPoints(
+ StackdriverExportUtils.createPoint(distributionData1, cumulativeData, DISTRIBUTION))
+ .build();
+ TimeSeries expected2 =
+ TimeSeries.newBuilder()
+ .setMetricKind(MetricKind.CUMULATIVE)
+ .setValueType(MetricDescriptor.ValueType.DISTRIBUTION)
+ .setMetric(
+ StackdriverExportUtils.createMetric(
+ view, Arrays.asList(VALUE_2), CUSTOM_OPENCENSUS_DOMAIN))
+ .setResource(MonitoredResource.newBuilder().setType("global"))
+ .addPoints(
+ StackdriverExportUtils.createPoint(distributionData2, cumulativeData, DISTRIBUTION))
+ .build();
+ assertThat(timeSeriesList).containsExactly(expected1, expected2);
+ }
+
+ @Test
+ public void createTimeSeriesList_interval() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ INTERVAL);
+ Map<List<TagValue>, DistributionData> aggregationMap =
+ ImmutableMap.of(
+ Arrays.asList(VALUE_1),
+ DistributionData.create(2, 3, 0, 5, 14, Arrays.asList(0L, 1L, 1L, 0L, 1L)),
+ Arrays.asList(VALUE_2),
+ DistributionData.create(-1, 1, -1, -1, 0, Arrays.asList(1L, 0L, 0L, 0L, 0L)));
+ ViewData viewData =
+ ViewData.create(view, aggregationMap, IntervalData.create(Timestamp.fromMillis(2000)));
+ assertThat(
+ StackdriverExportUtils.createTimeSeriesList(
+ viewData, DEFAULT_RESOURCE, CUSTOM_OPENCENSUS_DOMAIN))
+ .isEmpty();
+ }
+
+ @Test
+ public void createTimeSeriesList_withCustomMonitoredResource() {
+ MonitoredResource resource =
+ MonitoredResource.newBuilder().setType("global").putLabels("key", "value").build();
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ SUM,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ SumDataDouble sumData = SumDataDouble.create(55.5);
+ Map<List<TagValue>, SumDataDouble> aggregationMap =
+ ImmutableMap.of(Arrays.asList(VALUE_1), sumData);
+ CumulativeData cumulativeData =
+ CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000));
+ ViewData viewData = ViewData.create(view, aggregationMap, cumulativeData);
+ List<TimeSeries> timeSeriesList =
+ StackdriverExportUtils.createTimeSeriesList(viewData, resource, CUSTOM_OPENCENSUS_DOMAIN);
+ assertThat(timeSeriesList)
+ .containsExactly(
+ TimeSeries.newBuilder()
+ .setMetricKind(MetricKind.CUMULATIVE)
+ .setValueType(MetricDescriptor.ValueType.DOUBLE)
+ .setMetric(
+ StackdriverExportUtils.createMetric(
+ view, Arrays.asList(VALUE_1), CUSTOM_OPENCENSUS_DOMAIN))
+ .setResource(resource)
+ .addPoints(StackdriverExportUtils.createPoint(sumData, cumulativeData, SUM))
+ .build());
+ }
+}
diff --git a/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorkerTest.java b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorkerTest.java
new file mode 100644
index 00000000..27593829
--- /dev/null
+++ b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorkerTest.java
@@ -0,0 +1,310 @@
+/*
+ * 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.stats.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.stats.stackdriver.StackdriverExporterWorker.CUSTOM_OPENCENSUS_DOMAIN;
+import static io.opencensus.exporter.stats.stackdriver.StackdriverExporterWorker.DEFAULT_DISPLAY_NAME_PREFIX;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import com.google.api.MetricDescriptor;
+import com.google.api.MonitoredResource;
+import com.google.api.gax.rpc.UnaryCallable;
+import com.google.cloud.monitoring.v3.MetricServiceClient;
+import com.google.cloud.monitoring.v3.stub.MetricServiceStub;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.monitoring.v3.CreateMetricDescriptorRequest;
+import com.google.monitoring.v3.CreateTimeSeriesRequest;
+import com.google.monitoring.v3.TimeSeries;
+import com.google.protobuf.Empty;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import io.opencensus.stats.View.Name;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+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.MockitoAnnotations;
+
+/** Unit tests for {@link StackdriverExporterWorker}. */
+@RunWith(JUnit4.class)
+public class StackdriverExporterWorkerTest {
+
+ private static final String PROJECT_ID = "projectId";
+ private static final Duration ONE_SECOND = Duration.create(1, 0);
+ private static final TagKey KEY = TagKey.create("KEY");
+ private static final TagValue VALUE = TagValue.create("VALUE");
+ private static final String MEASURE_NAME = "my measurement";
+ private static final String MEASURE_UNIT = "us";
+ private static final String MEASURE_DESCRIPTION = "measure description";
+ private static final MeasureLong MEASURE =
+ MeasureLong.create(MEASURE_NAME, MEASURE_DESCRIPTION, MEASURE_UNIT);
+ private static final Name VIEW_NAME = Name.create("my view");
+ private static final String VIEW_DESCRIPTION = "view description";
+ private static final Cumulative CUMULATIVE = Cumulative.create();
+ private static final Interval INTERVAL = Interval.create(ONE_SECOND);
+ private static final Sum SUM = Sum.create();
+ private static final MonitoredResource DEFAULT_RESOURCE =
+ MonitoredResource.newBuilder().setType("global").build();
+
+ @Mock private ViewManager mockViewManager;
+
+ @Mock private MetricServiceStub mockStub;
+
+ @Mock
+ private UnaryCallable<CreateMetricDescriptorRequest, MetricDescriptor>
+ mockCreateMetricDescriptorCallable;
+
+ @Mock private UnaryCallable<CreateTimeSeriesRequest, Empty> mockCreateTimeSeriesCallable;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ doReturn(mockCreateMetricDescriptorCallable).when(mockStub).createMetricDescriptorCallable();
+ doReturn(mockCreateTimeSeriesCallable).when(mockStub).createTimeSeriesCallable();
+ doReturn(null)
+ .when(mockCreateMetricDescriptorCallable)
+ .call(any(CreateMetricDescriptorRequest.class));
+ doReturn(null).when(mockCreateTimeSeriesCallable).call(any(CreateTimeSeriesRequest.class));
+ }
+
+ @Test
+ public void testConstants() {
+ assertThat(StackdriverExporterWorker.MAX_BATCH_EXPORT_SIZE).isEqualTo(200);
+ assertThat(StackdriverExporterWorker.CUSTOM_METRIC_DOMAIN).isEqualTo("custom.googleapis.com/");
+ assertThat(StackdriverExporterWorker.CUSTOM_OPENCENSUS_DOMAIN)
+ .isEqualTo("custom.googleapis.com/opencensus/");
+ assertThat(StackdriverExporterWorker.DEFAULT_DISPLAY_NAME_PREFIX).isEqualTo("OpenCensus/");
+ }
+
+ @Test
+ public void export() throws IOException {
+ View view =
+ View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), CUMULATIVE);
+ ViewData viewData =
+ ViewData.create(
+ view,
+ ImmutableMap.of(Arrays.asList(VALUE), SumDataLong.create(1)),
+ CumulativeData.create(Timestamp.fromMillis(100), Timestamp.fromMillis(200)));
+ doReturn(ImmutableSet.of(view)).when(mockViewManager).getAllExportedViews();
+ doReturn(viewData).when(mockViewManager).getView(VIEW_NAME);
+
+ StackdriverExporterWorker worker =
+ new StackdriverExporterWorker(
+ PROJECT_ID,
+ new FakeMetricServiceClient(mockStub),
+ ONE_SECOND,
+ mockViewManager,
+ DEFAULT_RESOURCE,
+ null);
+ worker.export();
+
+ verify(mockStub, times(1)).createMetricDescriptorCallable();
+ verify(mockStub, times(1)).createTimeSeriesCallable();
+
+ MetricDescriptor descriptor =
+ StackdriverExportUtils.createMetricDescriptor(
+ view, PROJECT_ID, CUSTOM_OPENCENSUS_DOMAIN, DEFAULT_DISPLAY_NAME_PREFIX);
+ List<TimeSeries> timeSeries =
+ StackdriverExportUtils.createTimeSeriesList(
+ viewData, DEFAULT_RESOURCE, CUSTOM_OPENCENSUS_DOMAIN);
+ verify(mockCreateMetricDescriptorCallable, times(1))
+ .call(
+ eq(
+ CreateMetricDescriptorRequest.newBuilder()
+ .setName("projects/" + PROJECT_ID)
+ .setMetricDescriptor(descriptor)
+ .build()));
+ verify(mockCreateTimeSeriesCallable, times(1))
+ .call(
+ eq(
+ CreateTimeSeriesRequest.newBuilder()
+ .setName("projects/" + PROJECT_ID)
+ .addAllTimeSeries(timeSeries)
+ .build()));
+ }
+
+ @Test
+ public void doNotExportForEmptyViewData() {
+ View view =
+ View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), CUMULATIVE);
+ ViewData empty =
+ ViewData.create(
+ view,
+ Collections.<List<TagValue>, AggregationData>emptyMap(),
+ CumulativeData.create(Timestamp.fromMillis(100), Timestamp.fromMillis(200)));
+ doReturn(ImmutableSet.of(view)).when(mockViewManager).getAllExportedViews();
+ doReturn(empty).when(mockViewManager).getView(VIEW_NAME);
+
+ StackdriverExporterWorker worker =
+ new StackdriverExporterWorker(
+ PROJECT_ID,
+ new FakeMetricServiceClient(mockStub),
+ ONE_SECOND,
+ mockViewManager,
+ DEFAULT_RESOURCE,
+ null);
+
+ worker.export();
+ verify(mockStub, times(1)).createMetricDescriptorCallable();
+ verify(mockStub, times(0)).createTimeSeriesCallable();
+ }
+
+ @Test
+ public void doNotExportIfFailedToRegisterView() {
+ View view =
+ View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), CUMULATIVE);
+ doReturn(ImmutableSet.of(view)).when(mockViewManager).getAllExportedViews();
+ doThrow(new IllegalArgumentException()).when(mockStub).createMetricDescriptorCallable();
+ StackdriverExporterWorker worker =
+ new StackdriverExporterWorker(
+ PROJECT_ID,
+ new FakeMetricServiceClient(mockStub),
+ ONE_SECOND,
+ mockViewManager,
+ DEFAULT_RESOURCE,
+ null);
+
+ assertThat(worker.registerView(view)).isFalse();
+ worker.export();
+ verify(mockStub, times(1)).createMetricDescriptorCallable();
+ verify(mockStub, times(0)).createTimeSeriesCallable();
+ }
+
+ @Test
+ public void skipDifferentViewWithSameName() throws IOException {
+ StackdriverExporterWorker worker =
+ new StackdriverExporterWorker(
+ PROJECT_ID,
+ new FakeMetricServiceClient(mockStub),
+ ONE_SECOND,
+ mockViewManager,
+ DEFAULT_RESOURCE,
+ null);
+ View view1 =
+ View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), CUMULATIVE);
+ assertThat(worker.registerView(view1)).isTrue();
+ verify(mockStub, times(1)).createMetricDescriptorCallable();
+
+ View view2 =
+ View.create(
+ VIEW_NAME,
+ "This is a different description.",
+ MEASURE,
+ SUM,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ assertThat(worker.registerView(view2)).isFalse();
+ verify(mockStub, times(1)).createMetricDescriptorCallable();
+ }
+
+ @Test
+ public void doNotCreateMetricDescriptorForRegisteredView() {
+ StackdriverExporterWorker worker =
+ new StackdriverExporterWorker(
+ PROJECT_ID,
+ new FakeMetricServiceClient(mockStub),
+ ONE_SECOND,
+ mockViewManager,
+ DEFAULT_RESOURCE,
+ null);
+ View view =
+ View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), CUMULATIVE);
+ assertThat(worker.registerView(view)).isTrue();
+ verify(mockStub, times(1)).createMetricDescriptorCallable();
+
+ assertThat(worker.registerView(view)).isTrue();
+ verify(mockStub, times(1)).createMetricDescriptorCallable();
+ }
+
+ @Test
+ public void doNotCreateMetricDescriptorForIntervalView() {
+ StackdriverExporterWorker worker =
+ new StackdriverExporterWorker(
+ PROJECT_ID,
+ new FakeMetricServiceClient(mockStub),
+ ONE_SECOND,
+ mockViewManager,
+ DEFAULT_RESOURCE,
+ null);
+ View view =
+ View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), INTERVAL);
+ assertThat(worker.registerView(view)).isFalse();
+ verify(mockStub, times(0)).createMetricDescriptorCallable();
+ }
+
+ @Test
+ public void getDomain() {
+ assertThat(StackdriverExporterWorker.getDomain(null))
+ .isEqualTo("custom.googleapis.com/opencensus/");
+ assertThat(StackdriverExporterWorker.getDomain(""))
+ .isEqualTo("custom.googleapis.com/opencensus/");
+ assertThat(StackdriverExporterWorker.getDomain("custom.googleapis.com/myorg/"))
+ .isEqualTo("custom.googleapis.com/myorg/");
+ assertThat(StackdriverExporterWorker.getDomain("external.googleapis.com/prometheus/"))
+ .isEqualTo("external.googleapis.com/prometheus/");
+ assertThat(StackdriverExporterWorker.getDomain("myorg")).isEqualTo("myorg/");
+ }
+
+ @Test
+ public void getDisplayNamePrefix() {
+ assertThat(StackdriverExporterWorker.getDisplayNamePrefix(null)).isEqualTo("OpenCensus/");
+ assertThat(StackdriverExporterWorker.getDisplayNamePrefix("")).isEqualTo("");
+ assertThat(StackdriverExporterWorker.getDisplayNamePrefix("custom.googleapis.com/myorg/"))
+ .isEqualTo("custom.googleapis.com/myorg/");
+ assertThat(
+ StackdriverExporterWorker.getDisplayNamePrefix("external.googleapis.com/prometheus/"))
+ .isEqualTo("external.googleapis.com/prometheus/");
+ assertThat(StackdriverExporterWorker.getDisplayNamePrefix("myorg")).isEqualTo("myorg/");
+ }
+
+ /*
+ * MetricServiceClient.createMetricDescriptor() and MetricServiceClient.createTimeSeries() are
+ * final methods and cannot be mocked. We have to use a mock MetricServiceStub in order to verify
+ * the output.
+ */
+ private static final class FakeMetricServiceClient extends MetricServiceClient {
+
+ protected FakeMetricServiceClient(MetricServiceStub stub) {
+ super(stub);
+ }
+ }
+}
diff --git a/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfigurationTest.java b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfigurationTest.java
new file mode 100644
index 00000000..2d5eba1b
--- /dev/null
+++ b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfigurationTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.stats.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.api.MonitoredResource;
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.GoogleCredentials;
+import io.opencensus.common.Duration;
+import java.util.Date;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link StackdriverStatsConfiguration}. */
+@RunWith(JUnit4.class)
+public class StackdriverStatsConfigurationTest {
+
+ private static final Credentials FAKE_CREDENTIALS =
+ GoogleCredentials.newBuilder().setAccessToken(new AccessToken("fake", new Date(100))).build();
+ private static final String PROJECT_ID = "project";
+ private static final Duration DURATION = Duration.create(10, 0);
+ private static final MonitoredResource RESOURCE =
+ MonitoredResource.newBuilder()
+ .setType("gce-instance")
+ .putLabels("instance-id", "instance")
+ .build();
+ private static final String CUSTOM_PREFIX = "myorg";
+
+ @Test
+ public void testBuild() {
+ StackdriverStatsConfiguration configuration =
+ StackdriverStatsConfiguration.builder()
+ .setCredentials(FAKE_CREDENTIALS)
+ .setProjectId(PROJECT_ID)
+ .setExportInterval(DURATION)
+ .setMonitoredResource(RESOURCE)
+ .setMetricNamePrefix(CUSTOM_PREFIX)
+ .build();
+ assertThat(configuration.getCredentials()).isEqualTo(FAKE_CREDENTIALS);
+ assertThat(configuration.getProjectId()).isEqualTo(PROJECT_ID);
+ assertThat(configuration.getExportInterval()).isEqualTo(DURATION);
+ assertThat(configuration.getMonitoredResource()).isEqualTo(RESOURCE);
+ assertThat(configuration.getMetricNamePrefix()).isEqualTo(CUSTOM_PREFIX);
+ }
+
+ @Test
+ public void testBuild_Default() {
+ StackdriverStatsConfiguration configuration = StackdriverStatsConfiguration.builder().build();
+ assertThat(configuration.getCredentials()).isNull();
+ assertThat(configuration.getProjectId()).isNull();
+ assertThat(configuration.getExportInterval()).isNull();
+ assertThat(configuration.getMonitoredResource()).isNull();
+ assertThat(configuration.getMetricNamePrefix()).isNull();
+ }
+}
diff --git a/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporterTest.java b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporterTest.java
new file mode 100644
index 00000000..f5e3edd5
--- /dev/null
+++ b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporterTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.stats.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 io.opencensus.common.Duration;
+import java.io.IOException;
+import java.util.Date;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link StackdriverStatsExporter}. */
+@RunWith(JUnit4.class)
+public class StackdriverStatsExporterTest {
+
+ private static final String PROJECT_ID = "projectId";
+ private static final Duration ONE_SECOND = Duration.create(1, 0);
+ private static final Duration NEG_ONE_SECOND = Duration.create(-1, 0);
+ private static final Credentials FAKE_CREDENTIALS =
+ GoogleCredentials.newBuilder().setAccessToken(new AccessToken("fake", new Date(100))).build();
+ private static final StackdriverStatsConfiguration CONFIGURATION =
+ StackdriverStatsConfiguration.builder()
+ .setCredentials(FAKE_CREDENTIALS)
+ .setProjectId("project")
+ .build();
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testConstants() {
+ assertThat(StackdriverStatsExporter.DEFAULT_INTERVAL).isEqualTo(Duration.create(60, 0));
+ }
+
+ @Test
+ public void createWithNullStackdriverStatsConfiguration() throws IOException {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("configuration");
+ StackdriverStatsExporter.createAndRegister((StackdriverStatsConfiguration) null);
+ }
+
+ @Test
+ public void createWithNegativeDuration_WithConfiguration() throws IOException {
+ StackdriverStatsConfiguration configuration =
+ StackdriverStatsConfiguration.builder()
+ .setCredentials(FAKE_CREDENTIALS)
+ .setExportInterval(NEG_ONE_SECOND)
+ .build();
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Duration must be positive");
+ StackdriverStatsExporter.createAndRegister(configuration);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void createWithNullCredentials() throws IOException {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("credentials");
+ StackdriverStatsExporter.createAndRegisterWithCredentialsAndProjectId(
+ null, PROJECT_ID, ONE_SECOND);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void createWithNullProjectId() throws IOException {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("projectId");
+ StackdriverStatsExporter.createAndRegisterWithCredentialsAndProjectId(
+ GoogleCredentials.newBuilder().build(), null, ONE_SECOND);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void createWithNullDuration() throws IOException {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("exportInterval");
+ StackdriverStatsExporter.createAndRegisterWithCredentialsAndProjectId(
+ GoogleCredentials.newBuilder().build(), PROJECT_ID, null);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void createWithNegativeDuration() throws IOException {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Duration must be positive");
+ StackdriverStatsExporter.createAndRegisterWithCredentialsAndProjectId(
+ GoogleCredentials.newBuilder().build(), PROJECT_ID, NEG_ONE_SECOND);
+ }
+
+ @Test
+ public void createExporterTwice() throws IOException {
+ StackdriverStatsExporter.createAndRegister(CONFIGURATION);
+ try {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("Stackdriver stats exporter is already created.");
+ StackdriverStatsExporter.createAndRegister(CONFIGURATION);
+ } finally {
+ StackdriverStatsExporter.unsafeResetExporter();
+ }
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void createWithNullMonitoredResource() throws IOException {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("monitoredResource");
+ StackdriverStatsExporter.createAndRegisterWithMonitoredResource(ONE_SECOND, null);
+ }
+}
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"));
+ }
+}