diff options
author | Julien Desprez <jdesprez@google.com> | 2018-10-22 11:37:22 -0700 |
---|---|---|
committer | android-build-merger <android-build-merger@google.com> | 2018-10-22 11:37:22 -0700 |
commit | 13217871fefa43f6d16fbb31b04e9904996d87d5 (patch) | |
tree | ede84fcf0a9687d4907ae5f8a4788271d62e0922 /exporters | |
parent | cfbefd32336596ea63784607e4106dc37ce0567f (diff) | |
parent | 6fbc3cf5a1a3369fd354c1e5d9f90c86e4bce0a4 (diff) | |
download | opencensus-java-13217871fefa43f6d16fbb31b04e9904996d87d5.tar.gz |
Merge remote-tracking branch 'aosp/upstream-master' into merge am: dd3cabeacc
am: 6fbc3cf5a1
Change-Id: I11b0ec1cf561d2a14da78e444b1594f167787fe6
Diffstat (limited to 'exporters')
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")); + } +} |