diff options
10 files changed, 815 insertions, 0 deletions
diff --git a/RELEASING.md b/RELEASING.md index 2d42ed0b..675317e0 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -219,6 +219,7 @@ $ README_FILES=( contrib/grpc_util/README.md contrib/http_util/README.md contrib/zpages/README.md + exporters/stats/prometheus/README.md exporters/stats/signalfx/README.md exporters/stats/stackdriver/README.md exporters/trace/instana/README.md diff --git a/all/build.gradle b/all/build.gradle index 7ddfe177..3bc6c2b6 100644 --- a/all/build.gradle +++ b/all/build.gradle @@ -22,6 +22,7 @@ def subprojects = [ project(':opencensus-exporter-trace-zipkin'), project(':opencensus-exporter-stats-signalfx'), project(':opencensus-exporter-stats-stackdriver'), + project(':opencensus-exporter-stats-prometheus'), ] // A subset of subprojects for which we want to publish javadoc. @@ -38,6 +39,7 @@ def subprojects_javadoc = [ project(':opencensus-exporter-trace-zipkin'), project(':opencensus-exporter-stats-signalfx'), project(':opencensus-exporter-stats-stackdriver'), + project(':opencensus-exporter-stats-prometheus'), ] for (subproject in rootProject.subprojects) { diff --git a/build.gradle b/build.gradle index a305e91a..66cc725a 100644 --- a/build.gradle +++ b/build.gradle @@ -141,6 +141,7 @@ subprojects { googleCloudVersion = '0.33.0-beta' signalfxVersion = '0.0.39' zipkinReporterVersion = '2.0.0' + prometheusVersion = '0.2.0' libraries = [ auto_value: "com.google.auto.value:auto-value:${autoValueVersion}", @@ -160,6 +161,7 @@ subprojects { guava: "com.google.guava:guava:${guavaVersion}", jsr305: "com.google.code.findbugs:jsr305:${findBugsVersion}", signalfx_java: "com.signalfx.public:signalfx-java:${signalfxVersion}", + prometheus_simpleclient: "io.prometheus:simpleclient:${prometheusVersion}", // Test dependencies. guava_testlib: "com.google.guava:guava-testlib:${guavaVersion}", @@ -333,6 +335,7 @@ subprojects { 'opencensus-contrib-grpc-util', 'opencensus-contrib-http-util', 'opencensus-contrib-zpages', + 'opencensus-exporter-stats-prometheus', 'opencensus-exporter-stats-signalfx', 'opencensus-exporter-stats-stackdriver', 'opencensus-exporter-trace-instana', diff --git a/exporters/stats/prometheus/README.md b/exporters/stats/prometheus/README.md new file mode 100644 index 00000000..c506b077 --- /dev/null +++ b/exporters/stats/prometheus/README.md @@ -0,0 +1,58 @@ +# 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 + +TODO + +For Maven add to your `pom.xml`: + +For Gradle add to your dependencies: + +#### 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
\ No newline at end of file diff --git a/exporters/stats/prometheus/build.gradle b/exporters/stats/prometheus/build.gradle new file mode 100644 index 00000000..27ca1d70 --- /dev/null +++ b/exporters/stats/prometheus/build.gradle @@ -0,0 +1,15 @@ +description = 'OpenCensus Stats Prometheus Exporter' + +[compileJava, compileTestJava].each() { + it.sourceCompatibility = 1.7 + it.targetCompatibility = 1.7 +} + +dependencies { + compile project(':opencensus-api'), + libraries.prometheus_simpleclient + + testCompile project(':opencensus-api') + + signature "org.codehaus.mojo.signature:java17:+@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..4bc18826 --- /dev/null +++ b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtils.java @@ -0,0 +1,208 @@ +/* + * 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.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.Mean; +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.MeanData; +import io.opencensus.stats.AggregationData.SumDataDouble; +import io.opencensus.stats.AggregationData.SumDataLong; +import io.opencensus.stats.View; +import io.opencensus.stats.View.AggregationWindow; +import io.opencensus.stats.View.AggregationWindow.Cumulative; +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 {@link + * 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 Mean} will be + * {@link Type#SUMMARY} 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} and {@link CountData} will be converted to a single + * {@link Sample}. {@link 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. + */ +final class PrometheusExportUtils { + + @VisibleForTesting static final String OPENCENSUS_NAMESPACE = "opencensus"; + @VisibleForTesting static final String OPENCENSUS_HELP_MSG = "Opencensus Prometheus metrics: "; + @VisibleForTesting static final String SAMPLE_SUFFIX_BUCKET = "_bucket"; + @VisibleForTesting static final String SAMPLE_SUFFIX_COUNT = "_count"; + @VisibleForTesting static final String SAMPLE_SUFFIX_SUM = "_sum"; + + // Converts a ViewData to a Prometheus MetricFamilySamples. + static MetricFamilySamples createMetricFamilySamples(ViewData viewData) { + View view = viewData.getView(); + String name = + Collector.sanitizeMetricName(OPENCENSUS_NAMESPACE + '_' + view.getName().asString()); + Type type = getType(view.getAggregation(), view.getWindow()); + List<Sample> samples = Lists.newArrayList(); + for (Entry<List<TagValue>, AggregationData> entry : viewData.getAggregationMap().entrySet()) { + samples.addAll(getSamples(name, view.getColumns(), entry.getKey(), entry.getValue())); + } + return new MetricFamilySamples( + name, type, OPENCENSUS_HELP_MSG + 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(OPENCENSUS_NAMESPACE + '_' + view.getName().asString()); + Type type = getType(view.getAggregation(), view.getWindow()); + return new MetricFamilySamples( + name, type, OPENCENSUS_HELP_MSG + view.getDescription(), Collections.<Sample>emptyList()); + } + + @VisibleForTesting + static Type getType(Aggregation aggregation, AggregationWindow window) { + if (!(window instanceof Cumulative)) { + return Type.UNTYPED; + } + return aggregation.match( + Functions.returnConstant(Type.UNTYPED), // SUM + Functions.returnConstant(Type.COUNTER), // COUNT + Functions.returnConstant(Type.SUMMARY), // MEAN + Functions.returnConstant(Type.HISTOGRAM), // DISTRIBUTION + Functions.returnConstant(Type.UNTYPED)); + } + + @VisibleForTesting + static List<Sample> getSamples( + final String name, + List<TagKey> tagKeys, + List<TagValue> tagValues, + AggregationData aggregationData) { + Preconditions.checkArgument( + tagKeys.size() == tagValues.size(), "Tag keys and tag values have different sizes."); + final List<Sample> samples = Lists.newArrayList(); + final List<String> labelNames = new ArrayList<String>(tagKeys.size()); + final List<String> labelValues = new ArrayList<String>(tagValues.size()); + for (TagKey tagKey : tagKeys) { + labelNames.add(Collector.sanitizeMetricName(tagKey.getName())); + } + 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<MeanData, Void>() { + @Override + public Void apply(MeanData arg) { + 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<DistributionData, Void>() { + @Override + public Void apply(DistributionData arg) { + for (long bucketCount : arg.getBucketCounts()) { + samples.add( + new MetricFamilySamples.Sample( + name + SAMPLE_SUFFIX_BUCKET, labelNames, labelValues, bucketCount)); + } + 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; + } + }, + Functions.</*@Nullable*/ Void>throwAssertionError()); + + return samples; + } + + 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..8fcefb0c --- /dev/null +++ b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollector.java @@ -0,0 +1,89 @@ +/* + * 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.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import io.opencensus.stats.Stats; +import io.opencensus.stats.View; +import io.opencensus.stats.ViewData; +import io.opencensus.stats.ViewManager; +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 + */ +public final class PrometheusStatsCollector extends Collector implements Collector.Describable { + + private static final Logger logger = Logger.getLogger(PrometheusStatsCollector.class.getName()); + + private final ViewManager viewManager; + + /** + * Creates a {@link PrometheusStatsCollector} and registers it to Prometheus {@link + * CollectorRegistry#defaultRegistry}. + * + * @throws IllegalArgumentException if a {@code PrometheusStatsCollector} has already been created + * and registered. + * @since 0.12 + */ + public static void createAndRegister() { + new PrometheusStatsCollector(Stats.getViewManager()).register(); + } + + @Override + public List<MetricFamilySamples> collect() { + List<MetricFamilySamples> samples = Lists.newArrayList(); + try { + for (View view : viewManager.getAllExportedViews()) { + 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); + } + return samples; + } + + @Override + public List<MetricFamilySamples> describe() { + List<MetricFamilySamples> samples = Lists.newArrayList(); + try { + for (View view : viewManager.getAllExportedViews()) { + samples.add(PrometheusExportUtils.createDescribableMetricFamilySamples(view)); + } + } catch (Throwable e) { + logger.log(Level.WARNING, "Exception thrown when describing metrics.", e); + } + return samples; + } + + @VisibleForTesting + PrometheusStatsCollector(ViewManager viewManager) { + this.viewManager = viewManager; + } +} 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..9629328d --- /dev/null +++ b/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtilsTest.java @@ -0,0 +1,296 @@ +/* + * 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.OPENCENSUS_HELP_MSG; +import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.OPENCENSUS_NAMESPACE; +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 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.Mean; +import io.opencensus.stats.Aggregation.Sum; +import io.opencensus.stats.AggregationData.CountData; +import io.opencensus.stats.AggregationData.DistributionData; +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 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 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 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 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 = OPENCENSUS_NAMESPACE + "_view"; + + @Test + public void testConstants() { + assertThat(OPENCENSUS_NAMESPACE).isEqualTo("opencensus"); + assertThat(OPENCENSUS_HELP_MSG).isEqualTo("Opencensus Prometheus metrics: "); + assertThat(SAMPLE_SUFFIX_BUCKET).isEqualTo("_bucket"); + assertThat(SAMPLE_SUFFIX_COUNT).isEqualTo("_count"); + assertThat(SAMPLE_SUFFIX_SUM).isEqualTo("_sum"); + } + + @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); + } + + @Test + public void createDescribableMetricFamilySamples() { + assertThat(PrometheusExportUtils.createDescribableMetricFamilySamples(VIEW1)) + .isEqualTo( + new MetricFamilySamples( + OPENCENSUS_NAMESPACE + "_view1", + Type.COUNTER, + OPENCENSUS_HELP_MSG + DESCRIPTION, + Collections.<Sample>emptyList())); + assertThat(PrometheusExportUtils.createDescribableMetricFamilySamples(VIEW2)) + .isEqualTo( + new MetricFamilySamples( + OPENCENSUS_NAMESPACE + "_view2", + Type.SUMMARY, + OPENCENSUS_HELP_MSG + DESCRIPTION, + Collections.<Sample>emptyList())); + assertThat(PrometheusExportUtils.createDescribableMetricFamilySamples(VIEW3)) + .isEqualTo( + new MetricFamilySamples( + OPENCENSUS_NAMESPACE + "_view_3", + Type.HISTOGRAM, + OPENCENSUS_HELP_MSG + DESCRIPTION, + Collections.<Sample>emptyList())); + assertThat(PrometheusExportUtils.createDescribableMetricFamilySamples(VIEW4)) + .isEqualTo( + new MetricFamilySamples( + OPENCENSUS_NAMESPACE + "__view4", + Type.UNTYPED, + OPENCENSUS_HELP_MSG + DESCRIPTION, + Collections.<Sample>emptyList())); + } + + @Test + public void getSamples() { + assertThat( + PrometheusExportUtils.getSamples( + SAMPLE_NAME, Arrays.asList(K1, K2), Arrays.asList(V1, V2), SUM_DATA_DOUBLE)) + .containsExactly( + new Sample(SAMPLE_NAME, Arrays.asList("k1", "k2"), Arrays.asList("v1", "v2"), -5.5)); + assertThat( + PrometheusExportUtils.getSamples( + SAMPLE_NAME, Arrays.asList(K3), Arrays.asList(V3), SUM_DATA_LONG)) + .containsExactly( + new Sample(SAMPLE_NAME, Arrays.asList("k_3"), Arrays.asList("v-3"), 123456789)); + assertThat( + PrometheusExportUtils.getSamples( + SAMPLE_NAME, Arrays.asList(K1, K3), Arrays.asList(V1, null), COUNT_DATA)) + .containsExactly( + new Sample(SAMPLE_NAME, Arrays.asList("k1", "k_3"), Arrays.asList("v1", ""), 12345)); + assertThat( + PrometheusExportUtils.getSamples( + SAMPLE_NAME, Arrays.asList(K3), Arrays.asList(V3), MEAN_DATA)) + .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, Arrays.asList(K1), Arrays.asList(V1), DISTRIBUTION_DATA)) + .containsExactly( + new Sample(SAMPLE_NAME + "_bucket", Arrays.asList("k1"), Arrays.asList("v1"), 0), + new Sample(SAMPLE_NAME + "_bucket", Arrays.asList("k1"), Arrays.asList("v1"), 2), + new Sample(SAMPLE_NAME + "_bucket", Arrays.asList("k1"), Arrays.asList("v1"), 2), + new Sample(SAMPLE_NAME + "_bucket", Arrays.asList("k1"), Arrays.asList("v1"), 1), + 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(); + } + + @Test + public void getSamples_KeysAndValuesHaveDifferentSizes() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Tag keys and tag values have different sizes."); + PrometheusExportUtils.getSamples( + SAMPLE_NAME, Arrays.asList(K1, K2, K3), Arrays.asList(V1, V2), DISTRIBUTION_DATA); + } + + @Test + public void createMetricFamilySamples() { + assertThat( + PrometheusExportUtils.createMetricFamilySamples( + ViewData.create( + VIEW1, ImmutableMap.of(Arrays.asList(V1, V2), COUNT_DATA), CUMULATIVE_DATA))) + .isEqualTo( + new MetricFamilySamples( + OPENCENSUS_NAMESPACE + "_view1", + Type.COUNTER, + OPENCENSUS_HELP_MSG + DESCRIPTION, + Arrays.asList( + new Sample( + OPENCENSUS_NAMESPACE + "_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( + OPENCENSUS_NAMESPACE + "_view2", + Type.SUMMARY, + OPENCENSUS_HELP_MSG + DESCRIPTION, + Arrays.asList( + new Sample( + OPENCENSUS_NAMESPACE + "_view2_count", + Arrays.asList("k_3"), + Arrays.asList("v1"), + 22), + new Sample( + OPENCENSUS_NAMESPACE + "_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( + OPENCENSUS_NAMESPACE + "_view_3", + Type.HISTOGRAM, + OPENCENSUS_HELP_MSG + DESCRIPTION, + Arrays.asList( + new Sample( + OPENCENSUS_NAMESPACE + "_view_3_bucket", + Arrays.asList("k1"), + Arrays.asList("v-3"), + 0), + new Sample( + OPENCENSUS_NAMESPACE + "_view_3_bucket", + Arrays.asList("k1"), + Arrays.asList("v-3"), + 2), + new Sample( + OPENCENSUS_NAMESPACE + "_view_3_bucket", + Arrays.asList("k1"), + Arrays.asList("v-3"), + 2), + new Sample( + OPENCENSUS_NAMESPACE + "_view_3_bucket", + Arrays.asList("k1"), + Arrays.asList("v-3"), + 1), + new Sample( + OPENCENSUS_NAMESPACE + "_view_3_count", + Arrays.asList("k1"), + Arrays.asList("v-3"), + 5), + new Sample( + OPENCENSUS_NAMESPACE + "_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( + OPENCENSUS_NAMESPACE + "__view4", + Type.UNTYPED, + OPENCENSUS_HELP_MSG + DESCRIPTION, + Arrays.asList( + new Sample( + OPENCENSUS_NAMESPACE + "__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..b10164e7 --- /dev/null +++ b/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollectorTest.java @@ -0,0 +1,140 @@ +/* + * 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 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.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 {@link PrometheusStatsCollector}. */ +@RunWith(JUnit4.class) +public class PrometheusStatsCollectorTest { + + @Rule public final ExpectedException thrown = ExpectedException.none(); + + 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 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 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); + + @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 = "opencensus_view1"; + assertThat(collector.collect()) + .containsExactly( + new MetricFamilySamples( + "opencensus_view1", + Type.HISTOGRAM, + "Opencensus Prometheus metrics: View description", + Arrays.asList( + new Sample( + name + "_bucket", Arrays.asList("k1", "k2"), Arrays.asList("v1", "v2"), 0), + new Sample( + name + "_bucket", Arrays.asList("k1", "k2"), Arrays.asList("v1", "v2"), 2), + new Sample( + name + "_bucket", Arrays.asList("k1", "k2"), Arrays.asList("v1", "v2"), 2), + new Sample( + name + "_bucket", Arrays.asList("k1", "k2"), Arrays.asList("v1", "v2"), 1), + 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 testDescribe() { + PrometheusStatsCollector collector = new PrometheusStatsCollector(mockViewManager); + assertThat(collector.describe()) + .containsExactly( + new MetricFamilySamples( + "opencensus_view1", + Type.HISTOGRAM, + "Opencensus Prometheus metrics: 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/settings.gradle b/settings.gradle index 6921de56..093e0423 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,6 +11,7 @@ include ":opencensus-exporter-trace-stackdriver" include ":opencensus-exporter-trace-zipkin" include ":opencensus-exporter-stats-signalfx" include ":opencensus-exporter-stats-stackdriver" +include ":opencensus-exporter-stats-prometheus" include ":opencensus-contrib-agent" include ":opencensus-contrib-grpc-metrics" include ":opencensus-contrib-grpc-util" @@ -34,6 +35,8 @@ project(':opencensus-exporter-trace-stackdriver').projectDir = project(':opencensus-exporter-trace-zipkin').projectDir = "$rootDir/exporters/trace/zipkin" as File project(':opencensus-exporter-stats-signalfx').projectDir = "$rootDir/exporters/stats/signalfx" as File project(':opencensus-exporter-stats-stackdriver').projectDir = "$rootDir/exporters/stats/stackdriver" as File +project(':opencensus-exporter-stats-prometheus').projectDir = "$rootDir/exporters/stats/prometheus" as File + // Java8 projects only if (JavaVersion.current().isJava8Compatible()) { |