diff --git a/java/google/registry/monitoring/metrics/stackdriver/BUILD b/java/google/registry/monitoring/metrics/stackdriver/BUILD
new file mode 100644
index 000000000..dca3fbe57
--- /dev/null
+++ b/java/google/registry/monitoring/metrics/stackdriver/BUILD
@@ -0,0 +1,24 @@
+package(
+ default_visibility = ["//visibility:public"],
+)
+
+licenses(["notice"]) # Apache 2.0
+
+java_library(
+ name = "stackdriver",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//java/google/registry/monitoring/metrics",
+ "@com_google_api_client",
+ "@com_google_apis_google_api_services_monitoring",
+ "@com_google_appengine_api_1_0_sdk",
+ "@com_google_auto_value",
+ "@com_google_code_findbugs_jsr305",
+ "@com_google_dagger",
+ "@com_google_errorprone_error_prone_annotations",
+ "@com_google_guava",
+ "@com_google_http_client",
+ "@com_google_re2j",
+ "@joda_time",
+ ],
+)
diff --git a/java/google/registry/monitoring/metrics/stackdriver/StackdriverWriter.java b/java/google/registry/monitoring/metrics/stackdriver/StackdriverWriter.java
new file mode 100644
index 000000000..b8908cf04
--- /dev/null
+++ b/java/google/registry/monitoring/metrics/stackdriver/StackdriverWriter.java
@@ -0,0 +1,408 @@
+// Copyright 2016 The Nomulus Authors. All Rights Reserved.
+//
+// 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 google.registry.monitoring.metrics.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.client.googleapis.json.GoogleJsonResponseException;
+import com.google.api.services.monitoring.v3.Monitoring;
+import com.google.api.services.monitoring.v3.model.BucketOptions;
+import com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest;
+import com.google.api.services.monitoring.v3.model.Distribution;
+import com.google.api.services.monitoring.v3.model.Explicit;
+import com.google.api.services.monitoring.v3.model.Exponential;
+import com.google.api.services.monitoring.v3.model.LabelDescriptor;
+import com.google.api.services.monitoring.v3.model.Linear;
+import com.google.api.services.monitoring.v3.model.Metric;
+import com.google.api.services.monitoring.v3.model.MetricDescriptor;
+import com.google.api.services.monitoring.v3.model.MonitoredResource;
+import com.google.api.services.monitoring.v3.model.Point;
+import com.google.api.services.monitoring.v3.model.TimeInterval;
+import com.google.api.services.monitoring.v3.model.TimeSeries;
+import com.google.api.services.monitoring.v3.model.TypedValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.RateLimiter;
+import google.registry.monitoring.metrics.CustomFitter;
+import google.registry.monitoring.metrics.DistributionFitter;
+import google.registry.monitoring.metrics.ExponentialFitter;
+import google.registry.monitoring.metrics.IncrementableMetric;
+import google.registry.monitoring.metrics.LinearFitter;
+import google.registry.monitoring.metrics.MetricPoint;
+import google.registry.monitoring.metrics.MetricRegistryImpl;
+import google.registry.monitoring.metrics.MetricSchema.Kind;
+import google.registry.monitoring.metrics.MetricWriter;
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Queue;
+import java.util.logging.Logger;
+import javax.annotation.concurrent.NotThreadSafe;
+import javax.inject.Inject;
+import javax.inject.Named;
+import org.joda.time.DateTime;
+import org.joda.time.Interval;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+
+/**
+ * Metrics writer for Google Cloud Monitoring V3
+ *
+ *
This class communicates with the API via HTTP. In order to increase throughput and minimize
+ * CPU, it buffers points to be written until it has {@code maxPointsPerRequest} points buffered or
+ * until {@link #flush()} is called.
+ *
+ * @see Introduction to the Stackdriver
+ * Monitoring API
+ */
+// TODO(shikhman): add retry logic
+@NotThreadSafe
+public class StackdriverWriter implements MetricWriter {
+
+ /**
+ * A counter representing the total number of points pushed. Has {@link Kind} and metric value
+ * types as labels.
+ */
+ private static final IncrementableMetric pushedPoints =
+ MetricRegistryImpl.getDefault()
+ .newIncrementableMetric(
+ "/metrics/stackdriver/points_pushed",
+ "Count of points pushed to Stackdriver Monitoring API.",
+ "Points Pushed",
+ ImmutableSet.of(
+ google.registry.monitoring.metrics.LabelDescriptor.create("kind", "Metric Kind"),
+ google.registry.monitoring.metrics.LabelDescriptor.create(
+ "valueType", "Metric Value Type")));
+ private static final String METRIC_DOMAIN = "custom.googleapis.com";
+ private static final String LABEL_VALUE_TYPE = "STRING";
+ private static final DateTimeFormatter DATETIME_FORMATTER = ISODateTimeFormat.dateTime();
+ private static final Logger logger = Logger.getLogger(StackdriverWriter.class.getName());
+ // A map of native type to the equivalent Stackdriver metric type.
+ private static final ImmutableMap, String> ENCODED_METRIC_TYPES =
+ new ImmutableMap.Builder, String>()
+ .put(Long.class, "INT64")
+ .put(Double.class, "DOUBLE")
+ .put(Boolean.class, "BOOL")
+ .put(String.class, "STRING")
+ .put(google.registry.monitoring.metrics.Distribution.class, "DISTRIBUTION")
+ .build();
+ // A map of native kind to the equivalent Stackdriver metric kind.
+ private static final ImmutableMap ENCODED_METRIC_KINDS =
+ new ImmutableMap.Builder()
+ .put(Kind.GAUGE.name(), "GAUGE")
+ .put(Kind.CUMULATIVE.name(), "CUMULATIVE")
+ .build();
+ private static final String FLUSH_OVERFLOW_ERROR = "Cannot flush more than 200 points at a time";
+ private static final String METRIC_KIND_ERROR =
+ "Unrecognized metric kind, must be one of "
+ + Joiner.on(",").join(ENCODED_METRIC_KINDS.keySet());
+ private static final String METRIC_TYPE_ERROR =
+ "Unrecognized metric type, must be one of "
+ + Joiner.on(" ").join(ENCODED_METRIC_TYPES.keySet());
+ private static final String METRIC_LABEL_COUNT_ERROR =
+ "Metric label value count does not match its MetricDescriptor's label count";
+
+ private final MonitoredResource monitoredResource;
+ private final Queue timeSeriesBuffer;
+ /**
+ * A local cache of MetricDescriptors. A metric's metadata (name, kind, type, label definitions)
+ * must be registered before points for the metric can be pushed.
+ */
+ private final HashMap, MetricDescriptor>
+ registeredDescriptors = new HashMap<>();
+ private final String projectResource;
+ private final Monitoring monitoringClient;
+ private final int maxPointsPerRequest;
+ private final RateLimiter rateLimiter;
+
+ /**
+ * Constructs a {@link StackdriverWriter}.
+ *
+ * The monitoringClient must have read and write permissions to the Cloud Monitoring API v3 on
+ * the provided project.
+ */
+ @Inject
+ public StackdriverWriter(
+ Monitoring monitoringClient,
+ @Named("stackdriverGcpProject") String project,
+ MonitoredResource monitoredResource,
+ @Named("stackdriverMaxQps") int maxQps,
+ @Named("stackdriverMaxPointsPerRequest") int maxPointsPerRequest) {
+ this.monitoringClient = checkNotNull(monitoringClient);
+ this.projectResource = "projects/" + checkNotNull(project);
+ this.monitoredResource = monitoredResource;
+ this.maxPointsPerRequest = maxPointsPerRequest;
+ this.timeSeriesBuffer = new ArrayDeque<>(maxPointsPerRequest);
+ this.rateLimiter = RateLimiter.create(maxQps);
+ }
+
+ @VisibleForTesting
+ static ImmutableList encodeLabelDescriptors(
+ ImmutableSet labelDescriptors) {
+ List stackDriverLabelDescriptors = new ArrayList<>(labelDescriptors.size());
+
+ for (google.registry.monitoring.metrics.LabelDescriptor labelDescriptor : labelDescriptors) {
+ stackDriverLabelDescriptors.add(
+ new LabelDescriptor()
+ .setKey(labelDescriptor.name())
+ .setDescription(labelDescriptor.description())
+ .setValueType(LABEL_VALUE_TYPE));
+ }
+
+ return ImmutableList.copyOf(stackDriverLabelDescriptors);
+ }
+
+ @VisibleForTesting
+ static MetricDescriptor encodeMetricDescriptor(
+ google.registry.monitoring.metrics.Metric> metric) {
+ return new MetricDescriptor()
+ .setType(METRIC_DOMAIN + metric.getMetricSchema().name())
+ .setDescription(metric.getMetricSchema().description())
+ .setDisplayName(metric.getMetricSchema().valueDisplayName())
+ .setValueType(ENCODED_METRIC_TYPES.get(metric.getValueClass()))
+ .setLabels(encodeLabelDescriptors(metric.getMetricSchema().labels()))
+ .setMetricKind(ENCODED_METRIC_KINDS.get(metric.getMetricSchema().kind().name()));
+ }
+
+ /** Encodes and writes a metric point to Stackdriver. The point may be buffered. */
+ @Override
+ public void write(google.registry.monitoring.metrics.MetricPoint point)
+ throws IOException {
+ checkNotNull(point);
+
+ TimeSeries timeSeries = getEncodedTimeSeries(point);
+ timeSeriesBuffer.add(timeSeries);
+
+ logger.fine(String.format("Enqueued metric %s for writing", timeSeries.getMetric().getType()));
+ if (timeSeriesBuffer.size() == maxPointsPerRequest) {
+ flush();
+ }
+ }
+
+ /** Flushes all buffered metric points to Stackdriver. This call is blocking. */
+ @Override
+ public void flush() throws IOException {
+ checkState(timeSeriesBuffer.size() <= 200, FLUSH_OVERFLOW_ERROR);
+
+ // Return early; Stackdriver throws errors if we attempt to send empty requests.
+ if (timeSeriesBuffer.isEmpty()) {
+ logger.fine("Attempted to flush with no pending points, doing nothing");
+ return;
+ }
+
+ ImmutableList timeSeriesList = ImmutableList.copyOf(timeSeriesBuffer);
+ timeSeriesBuffer.clear();
+
+ CreateTimeSeriesRequest request = new CreateTimeSeriesRequest().setTimeSeries(timeSeriesList);
+
+ rateLimiter.acquire();
+ monitoringClient.projects().timeSeries().create(projectResource, request).execute();
+
+ for (TimeSeries timeSeries : timeSeriesList) {
+ pushedPoints.increment(timeSeries.getMetricKind(), timeSeries.getValueType());
+ }
+ logger.info(String.format("Flushed %d metrics to Stackdriver", timeSeriesList.size()));
+ }
+
+ /**
+ * Registers a metric's {@link MetricDescriptor} with the Monitoring API.
+ *
+ * @param metric the metric to be registered.
+ * @see Stackdriver MetricDescriptor API
+ */
+ @VisibleForTesting
+ MetricDescriptor registerMetric(final google.registry.monitoring.metrics.Metric> metric)
+ throws IOException {
+ if (registeredDescriptors.containsKey(metric)) {
+ logger.fine(
+ String.format("Using existing metric descriptor %s", metric.getMetricSchema().name()));
+ return registeredDescriptors.get(metric);
+ }
+
+ MetricDescriptor descriptor = encodeMetricDescriptor(metric);
+
+ rateLimiter.acquire();
+ // We try to create a descriptor, but it may have been created already, so we re-fetch it on
+ // failure
+ try {
+ descriptor =
+ monitoringClient
+ .projects()
+ .metricDescriptors()
+ .create(projectResource, descriptor)
+ .execute();
+ logger.info(String.format("Registered new metric descriptor %s", descriptor.getType()));
+ } catch (GoogleJsonResponseException jsonException) {
+ // Not the error we were expecting, just give up
+ if (!"ALREADY_EXISTS".equals(jsonException.getStatusMessage())) {
+ throw jsonException;
+ }
+
+ descriptor =
+ monitoringClient
+ .projects()
+ .metricDescriptors()
+ .get(projectResource + "/metricDescriptors/" + descriptor.getType())
+ .execute();
+
+ logger.info(
+ String.format("Fetched existing metric descriptor %s", metric.getMetricSchema().name()));
+ }
+
+ registeredDescriptors.put(metric, descriptor);
+
+ return descriptor;
+ }
+
+ private static TimeInterval encodeTimeInterval(Interval nativeInterval, Kind metricKind) {
+
+ TimeInterval encodedInterval =
+ new TimeInterval().setStartTime(DATETIME_FORMATTER.print(nativeInterval.getStart()));
+
+ DateTime endTimestamp =
+ nativeInterval.toDurationMillis() == 0 && metricKind != Kind.GAUGE
+ ? nativeInterval.getEnd().plusMillis(1)
+ : nativeInterval.getEnd();
+
+ return encodedInterval.setEndTime(DATETIME_FORMATTER.print(endTimestamp));
+ }
+
+ private static BucketOptions encodeBucketOptions(DistributionFitter fitter) {
+ BucketOptions bucketOptions = new BucketOptions();
+
+ if (fitter instanceof LinearFitter) {
+ LinearFitter linearFitter = (LinearFitter) fitter;
+
+ bucketOptions.setLinearBuckets(
+ new Linear()
+ .setNumFiniteBuckets(linearFitter.boundaries().size() - 1)
+ .setWidth(linearFitter.width())
+ .setOffset(linearFitter.offset()));
+ } else if (fitter instanceof ExponentialFitter) {
+ ExponentialFitter exponentialFitter = (ExponentialFitter) fitter;
+
+ bucketOptions.setExponentialBuckets(
+ new Exponential()
+ .setNumFiniteBuckets(exponentialFitter.boundaries().size() - 1)
+ .setGrowthFactor(exponentialFitter.base())
+ .setScale(exponentialFitter.scale()));
+ } else if (fitter instanceof CustomFitter) {
+ bucketOptions.setExplicitBuckets(new Explicit().setBounds(fitter.boundaries().asList()));
+ } else {
+ throw new IllegalArgumentException("Illegal DistributionFitter type");
+ }
+
+ return bucketOptions;
+ }
+
+ private static List encodeDistributionPoints(
+ google.registry.monitoring.metrics.Distribution distribution) {
+ return distribution.intervalCounts().asMapOfRanges().values().asList();
+ }
+
+ private static Distribution encodeDistribution(
+ google.registry.monitoring.metrics.Distribution nativeDistribution) {
+ return new Distribution()
+ .setMean(nativeDistribution.mean())
+ .setCount(nativeDistribution.count())
+ .setSumOfSquaredDeviation(nativeDistribution.sumOfSquaredDeviation())
+ .setBucketOptions(encodeBucketOptions(nativeDistribution.distributionFitter()))
+ .setBucketCounts(encodeDistributionPoints(nativeDistribution));
+ }
+
+ /**
+ * Encodes a {@link MetricPoint} into a Stackdriver {@link TimeSeries}.
+ *
+ * This method will register the underlying {@link google.registry.monitoring.metrics.Metric}
+ * as a Stackdriver {@link MetricDescriptor}.
+ *
+ * @see
+ * Stackdriver TimeSeries API
+ */
+ @VisibleForTesting
+ TimeSeries getEncodedTimeSeries(google.registry.monitoring.metrics.MetricPoint point)
+ throws IOException {
+ google.registry.monitoring.metrics.Metric metric = point.metric();
+ try {
+ checkArgument(
+ ENCODED_METRIC_KINDS.containsKey(metric.getMetricSchema().kind().name()),
+ METRIC_KIND_ERROR);
+ checkArgument(ENCODED_METRIC_TYPES.containsKey(metric.getValueClass()), METRIC_TYPE_ERROR);
+ } catch (IllegalArgumentException e) {
+ throw new IOException(e.getMessage());
+ }
+
+ MetricDescriptor descriptor = registerMetric(metric);
+
+ if (point.labelValues().size() != point.metric().getMetricSchema().labels().size()) {
+ throw new IOException(METRIC_LABEL_COUNT_ERROR);
+ }
+
+ V value = point.value();
+ TypedValue encodedValue = new TypedValue();
+ Class> valueClass = metric.getValueClass();
+
+ if (valueClass == Long.class) {
+ encodedValue.setInt64Value((Long) value);
+ } else if (valueClass == Double.class) {
+ encodedValue.setDoubleValue((Double) value);
+ } else if (valueClass == Boolean.class) {
+ encodedValue.setBoolValue((Boolean) value);
+ } else if (valueClass == String.class) {
+ encodedValue.setStringValue((String) value);
+ } else if (valueClass == google.registry.monitoring.metrics.Distribution.class) {
+ encodedValue.setDistributionValue(
+ encodeDistribution((google.registry.monitoring.metrics.Distribution) value));
+ } else {
+ // This is unreachable because the precondition checks will catch all NotSerializable
+ // exceptions.
+ throw new IllegalArgumentException("Invalid metric valueClass: " + metric.getValueClass());
+ }
+
+ Point encodedPoint =
+ new Point()
+ .setInterval(encodeTimeInterval(point.interval(), metric.getMetricSchema().kind()))
+ .setValue(encodedValue);
+
+ List encodedLabels = descriptor.getLabels();
+ // The MetricDescriptors returned by the GCM API have null fields rather than empty lists
+ encodedLabels = encodedLabels == null ? ImmutableList.of() : encodedLabels;
+
+ ImmutableMap.Builder labelValues = new ImmutableMap.Builder<>();
+ int i = 0;
+ for (LabelDescriptor labelDescriptor : encodedLabels) {
+ labelValues.put(labelDescriptor.getKey(), point.labelValues().get(i++));
+ }
+
+ Metric encodedMetric =
+ new Metric().setType(descriptor.getType()).setLabels(labelValues.build());
+
+ return new TimeSeries()
+ .setMetric(encodedMetric)
+ .setPoints(ImmutableList.of(encodedPoint))
+ .setResource(monitoredResource)
+ // these two attributes are ignored by the API, we set them here to use elsewhere
+ // for internal metrics.
+ .setMetricKind(descriptor.getMetricKind())
+ .setValueType(descriptor.getValueType());
+ }
+}
diff --git a/javatests/google/registry/monitoring/metrics/example/BUILD b/javatests/google/registry/monitoring/metrics/example/BUILD
new file mode 100644
index 000000000..ff6127479
--- /dev/null
+++ b/javatests/google/registry/monitoring/metrics/example/BUILD
@@ -0,0 +1,19 @@
+package(
+ default_visibility = ["//java/google/registry:registry_project"],
+)
+
+licenses(["notice"]) # Apache 2.0
+
+java_binary(
+ name = "SheepCounterExample",
+ srcs = ["SheepCounterExample.java"],
+ deps = [
+ "//java/google/registry/monitoring/metrics",
+ "//java/google/registry/monitoring/metrics/stackdriver",
+ "@com_google_api_client",
+ "@com_google_apis_google_api_services_monitoring",
+ "@com_google_guava",
+ "@com_google_http_client",
+ "@com_google_http_client_jackson2",
+ ],
+)
diff --git a/javatests/google/registry/monitoring/metrics/example/SheepCounterExample.java b/javatests/google/registry/monitoring/metrics/example/SheepCounterExample.java
new file mode 100644
index 000000000..c9b58fd15
--- /dev/null
+++ b/javatests/google/registry/monitoring/metrics/example/SheepCounterExample.java
@@ -0,0 +1,268 @@
+// Copyright 2016 The Nomulus Authors. All Rights Reserved.
+//
+// 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 google.registry.monitoring.metrics.example;
+
+import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.services.monitoring.v3.Monitoring;
+import com.google.api.services.monitoring.v3.MonitoringScopes;
+import com.google.api.services.monitoring.v3.model.MonitoredResource;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import google.registry.monitoring.metrics.EventMetric;
+import google.registry.monitoring.metrics.IncrementableMetric;
+import google.registry.monitoring.metrics.LabelDescriptor;
+import google.registry.monitoring.metrics.LinearFitter;
+import google.registry.monitoring.metrics.Metric;
+import google.registry.monitoring.metrics.MetricRegistryImpl;
+import google.registry.monitoring.metrics.MetricReporter;
+import google.registry.monitoring.metrics.MetricWriter;
+import google.registry.monitoring.metrics.SettableMetric;
+import google.registry.monitoring.metrics.stackdriver.StackdriverWriter;
+import java.io.IOException;
+import java.util.Random;
+import java.util.concurrent.Executors;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
+
+/** A sample application which uses the Metrics API to count sheep while sleeping. */
+public final class SheepCounterExample {
+
+ /**
+ * The code below for using a custom {@link LogManager} is only necessary to enable logging at JVM
+ * shutdown to show the shutdown logs of {@link MetricReporter} in this small standalone
+ * application.
+ *
+ * It is NOT necessary for normal use of the Metrics library.
+ */
+ static {
+ // must be called before any Logger method is used.
+ System.setProperty("java.util.logging.manager", DelayedShutdownLogManager.class.getName());
+ }
+
+ private static final Logger logger = Logger.getLogger(SheepCounterExample.class.getName());
+
+ /**
+ * The time interval, in seconds, between when the {@link MetricReporter} will read {@link Metric}
+ * instances and enqueue them to the {@link MetricWriter}.
+ *
+ * @see MetricReporter
+ */
+ private static final long METRICS_REPORTING_INTERVAL = 30L;
+
+ /**
+ * The maximum queries per second to the Stackdriver API. Contact Cloud Support to raise this from
+ * the default value if necessary.
+ */
+ private static final int STACKDRIVER_MAX_QPS = 30;
+
+ /**
+ * The maximum number of {@link com.google.api.services.monitoring.v3.model.TimeSeries} that can
+ * be bundled into a single {@link
+ * com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest}. This must be at most 200.
+ * Setting this lower will cause the {@link StackdriverWriter} to {@link
+ * StackdriverWriter#flush()} more frequently.
+ */
+ private static final int STACKDRIVER_MAX_POINTS_PER_REQUEST = 200;
+
+ // Create some metrics to track your ZZZs.
+ private static final ImmutableList SHEEP_COLORS =
+ ImmutableList.of("Green", "Yellow", "Red", "Blue");
+ private static final ImmutableList SHEEP_SPECIES =
+ ImmutableList.of("Domestic", "Bighorn");
+ private static final ImmutableSet SHEEP_ATTRIBUTES =
+ ImmutableSet.of(
+ LabelDescriptor.create("color", "Sheep Color"),
+ LabelDescriptor.create("species", "Sheep Species"));
+
+ /**
+ * Counters are good for tracking monotonically increasing values, like request counts or error
+ * counts. Or, in this case, sheep.
+ */
+ private static final IncrementableMetric sheepCounter =
+ MetricRegistryImpl.getDefault()
+ .newIncrementableMetric(
+ "/sheep", "Counts sheep over time.", "Number of Sheep", SHEEP_ATTRIBUTES);
+
+ /**
+ * Settable metrics are good for state indicators. For example, you could use one to track the
+ * lifecycle of a {@link com.google.common.util.concurrent.Service}. In this case, we are just
+ * using it to track the sleep state of this application.
+ */
+ private static final SettableMetric isSleeping =
+ MetricRegistryImpl.getDefault()
+ .newSettableMetric(
+ "/is_sleeping",
+ "Tracks sleep state.",
+ "Sleeping?",
+ ImmutableSet.of(),
+ Boolean.class);
+
+ /**
+ * Gauge metrics never need to be accessed, so the assignment here is unnecessary. You only need
+ * it if you plan on calling {@link Metric#getTimestampedValues()} to read the values of the
+ * metric in the code yourself.
+ */
+ @SuppressWarnings("unused")
+ private static final Metric sleepQuality =
+ MetricRegistryImpl.getDefault()
+ .newGauge(
+ "/sleep_quality",
+ "Quality of the sleep.",
+ "Quality",
+ ImmutableSet.of(),
+ new Supplier, Double>>() {
+ @Override
+ public ImmutableMap, Double> get() {
+ return ImmutableMap.of(ImmutableList.of(), new Random().nextDouble());
+ }
+ },
+ Double.class);
+
+ /**
+ * Event metrics track aspects of an "event." Here, we track the fluffiness of the sheep we've
+ * seen.
+ */
+ private static final EventMetric sheepFluffiness =
+ MetricRegistryImpl.getDefault()
+ .newEventMetric(
+ "/sheep_fluffiness",
+ "Measures the fluffiness of seen sheep.",
+ "Fill Power",
+ SHEEP_ATTRIBUTES,
+ LinearFitter.create(5, 20.0, 20.0));
+
+ private static Monitoring createAuthorizedMonitoringClient() throws IOException {
+ // Grab the Application Default Credentials from the environment.
+ // Generate these with 'gcloud beta auth application-default login'
+ GoogleCredential credential =
+ GoogleCredential.getApplicationDefault().createScoped(MonitoringScopes.all());
+
+ // Create and return the CloudMonitoring service object
+ HttpTransport httpTransport = new NetHttpTransport();
+ JsonFactory jsonFactory = new JacksonFactory();
+ return new Monitoring.Builder(httpTransport, jsonFactory, credential)
+ .setApplicationName("Monitoring Sample")
+ .build();
+ }
+
+ public static void main(String[] args) throws Exception {
+ if (args.length < 1) {
+ System.err.println("Missing required project argument");
+ System.err.printf(
+ "Usage: java %s gcp-project-id [verbose]\n", SheepCounterExample.class.getName());
+ return;
+ }
+ String project = args[0];
+
+ // Turn up the logging verbosity
+ if (args.length > 1) {
+ Logger log = LogManager.getLogManager().getLogger("");
+ log.setLevel(Level.ALL);
+ for (Handler h : log.getHandlers()) {
+ h.setLevel(Level.ALL);
+ }
+ }
+
+ // Create a sample resource. In this case, a GCE Instance.
+ // See https://cloud.google.com/monitoring/api/resources for a list of resource types.
+ MonitoredResource monitoredResource =
+ new MonitoredResource()
+ .setType("gce_instance")
+ .setLabels(
+ ImmutableMap.of(
+ "instance_id", "test-instance",
+ "zone", "us-central1-f"));
+
+ // Set up the Metrics infrastructure.
+ MetricWriter stackdriverWriter =
+ new StackdriverWriter(
+ createAuthorizedMonitoringClient(),
+ project,
+ monitoredResource,
+ STACKDRIVER_MAX_QPS,
+ STACKDRIVER_MAX_POINTS_PER_REQUEST);
+ final MetricReporter reporter =
+ new MetricReporter(
+ stackdriverWriter, METRICS_REPORTING_INTERVAL, Executors.defaultThreadFactory());
+ reporter.startAsync().awaitRunning();
+
+ // Set up a handler to stop sleeping on SIGINT.
+ Runtime.getRuntime()
+ .addShutdownHook(
+ new Thread(
+ new Runnable() {
+ @Override
+ public void run() {
+ reporter.stopAsync().awaitTerminated();
+ // Allow the LogManager to cleanup the loggers.
+ DelayedShutdownLogManager.resetFinally();
+ }
+ }));
+
+ System.err.println("Send SIGINT (Ctrl+C) to stop sleeping.");
+ while (true) {
+ // Count some Googley sheep.
+ int colorIndex = new Random().nextInt(SHEEP_COLORS.size());
+ int speciesIndex = new Random().nextInt(SHEEP_SPECIES.size());
+ sheepCounter.incrementBy(1, SHEEP_COLORS.get(colorIndex), SHEEP_SPECIES.get(speciesIndex));
+ sheepFluffiness.record(
+ new Random().nextDouble() * 200,
+ SHEEP_COLORS.get(colorIndex),
+ SHEEP_SPECIES.get(speciesIndex));
+ isSleeping.set(true);
+
+ logger.info("zzz...");
+ Thread.sleep(5000);
+ }
+ }
+
+ /**
+ * Special {@link LogManager} with a no-op {@link LogManager#reset()} so that logging can proceed
+ * as usual until stopped in in another runtime shutdown hook.
+ *
+ * The class is marked public because it is loaded by the JVM classloader at runtime.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static class DelayedShutdownLogManager extends LogManager {
+
+ private static DelayedShutdownLogManager instance;
+
+ public DelayedShutdownLogManager() {
+ instance = this;
+ }
+
+ /** A no-op implementation. */
+ @Override
+ public void reset() {
+ /* don't reset yet. */
+ }
+
+ static void resetFinally() {
+ instance.delayedReset();
+ }
+
+ private void delayedReset() {
+ super.reset();
+ }
+ }
+}
diff --git a/javatests/google/registry/monitoring/metrics/stackdriver/BUILD b/javatests/google/registry/monitoring/metrics/stackdriver/BUILD
new file mode 100644
index 000000000..76a0b3b8d
--- /dev/null
+++ b/javatests/google/registry/monitoring/metrics/stackdriver/BUILD
@@ -0,0 +1,34 @@
+package(
+ default_testonly = 1,
+ default_visibility = ["//java/google/registry:registry_project"],
+)
+
+licenses(["notice"]) # Apache 2.0
+
+load("//java/com/google/testing/builddefs:GenTestRules.bzl", "GenTestRules")
+
+java_library(
+ name = "stackdriver",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//java/google/registry/monitoring/metrics",
+ "//java/google/registry/monitoring/metrics/stackdriver",
+ "@com_google_api_client",
+ "@com_google_apis_google_api_services_monitoring",
+ "@com_google_guava",
+ "@com_google_http_client",
+ "@com_google_http_client_jackson2",
+ "@com_google_truth",
+ "@joda_time",
+ "@junit",
+ "@org_mockito_all",
+ ],
+)
+
+GenTestRules(
+ name = "GeneratedTestRules",
+ test_files = glob(["*Test.java"]),
+ deps = [
+ ":stackdriver",
+ ],
+)
diff --git a/javatests/google/registry/monitoring/metrics/stackdriver/GoogleJsonResponseExceptionHelper.java b/javatests/google/registry/monitoring/metrics/stackdriver/GoogleJsonResponseExceptionHelper.java
new file mode 100644
index 000000000..b3f918ece
--- /dev/null
+++ b/javatests/google/registry/monitoring/metrics/stackdriver/GoogleJsonResponseExceptionHelper.java
@@ -0,0 +1,171 @@
+// Copyright 2016 The Nomulus Authors. All Rights Reserved.
+//
+// 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 google.registry.monitoring.metrics.stackdriver;
+
+import com.google.api.client.googleapis.json.GoogleJsonResponseException;
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpContent;
+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.HttpTransport;
+import com.google.api.client.http.LowLevelHttpRequest;
+import com.google.api.client.http.LowLevelHttpResponse;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/** A helper to create instances of {@link GoogleJsonResponseException}. */
+public class GoogleJsonResponseExceptionHelper {
+ /**
+ * @param statusCode the status code that should be in the returned {@link
+ * GoogleJsonResponseException}
+ * @return a {@link GoogleJsonResponseException} with the status code {@code statusCode}
+ * @throws IOException shouldn't occur
+ */
+ public static GoogleJsonResponseException create(int statusCode) throws IOException {
+ HttpResponse response = createHttpResponse(statusCode, null);
+ return GoogleJsonResponseException.from(new JacksonFactory(), response);
+ }
+
+ public static HttpResponse createHttpResponse(int statusCode, InputStream content)
+ throws IOException {
+ FakeHttpTransport transport = new FakeHttpTransport(statusCode, content);
+ HttpRequestFactory factory = transport.createRequestFactory();
+ HttpRequest request =
+ factory.buildRequest(
+ "foo", new GenericUrl("http://example.com/bar"), new EmptyHttpContent());
+ request.setThrowExceptionOnExecuteError(false);
+ return request.execute();
+ }
+
+ private static class FakeHttpTransport extends HttpTransport {
+ private final int statusCode;
+ private final InputStream content;
+
+ FakeHttpTransport(int statusCode, InputStream content) {
+ this.statusCode = statusCode;
+ this.content = content;
+ }
+
+ @Override
+ protected LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
+ return new FakeLowLevelHttpRequest(statusCode, content);
+ }
+ }
+
+ private static class FakeLowLevelHttpRequest extends LowLevelHttpRequest {
+ private final int statusCode;
+ private final InputStream content;
+
+ FakeLowLevelHttpRequest(int statusCode, InputStream content) {
+ this.statusCode = statusCode;
+ this.content = content;
+ }
+
+ @Override
+ public void addHeader(String name, String value) throws IOException {
+ // Nothing!
+ }
+
+ @Override
+ public LowLevelHttpResponse execute() throws IOException {
+ return new FakeLowLevelHttpResponse(statusCode, content);
+ }
+ }
+
+ private static class FakeLowLevelHttpResponse extends LowLevelHttpResponse {
+ private final int statusCode;
+ private final InputStream content;
+
+ FakeLowLevelHttpResponse(int statusCode, InputStream content) {
+ this.statusCode = statusCode;
+ this.content = content;
+ }
+
+ @Override
+ public InputStream getContent() throws IOException {
+ return content;
+ }
+
+ @Override
+ public String getContentEncoding() throws IOException {
+ return null;
+ }
+
+ @Override
+ public long getContentLength() throws IOException {
+ return 0;
+ }
+
+ @Override
+ public String getContentType() throws IOException {
+ return "text/json";
+ }
+
+ @Override
+ public String getStatusLine() throws IOException {
+ return null;
+ }
+
+ @Override
+ public int getStatusCode() throws IOException {
+ return statusCode;
+ }
+
+ @Override
+ public String getReasonPhrase() throws IOException {
+ return null;
+ }
+
+ @Override
+ public int getHeaderCount() throws IOException {
+ return 0;
+ }
+
+ @Override
+ public String getHeaderName(int index) throws IOException {
+ return null;
+ }
+
+ @Override
+ public String getHeaderValue(int index) throws IOException {
+ return null;
+ }
+ }
+
+ private static class EmptyHttpContent implements HttpContent {
+ @Override
+ public long getLength() throws IOException {
+ return 0;
+ }
+
+ @Override
+ public String getType() {
+ return "text/json";
+ }
+
+ @Override
+ public boolean retrySupported() {
+ return false;
+ }
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException {
+ // Nothing!
+ }
+ }
+}
diff --git a/javatests/google/registry/monitoring/metrics/stackdriver/StackdriverWriterTest.java b/javatests/google/registry/monitoring/metrics/stackdriver/StackdriverWriterTest.java
new file mode 100644
index 000000000..4dea28353
--- /dev/null
+++ b/javatests/google/registry/monitoring/metrics/stackdriver/StackdriverWriterTest.java
@@ -0,0 +1,563 @@
+// Copyright 2016 The Nomulus Authors. All Rights Reserved.
+//
+// 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 google.registry.monitoring.metrics.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.api.client.googleapis.json.GoogleJsonResponseException;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.HttpResponseException;
+import com.google.api.services.monitoring.v3.Monitoring;
+import com.google.api.services.monitoring.v3.model.BucketOptions;
+import com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest;
+import com.google.api.services.monitoring.v3.model.Explicit;
+import com.google.api.services.monitoring.v3.model.Exponential;
+import com.google.api.services.monitoring.v3.model.Linear;
+import com.google.api.services.monitoring.v3.model.MetricDescriptor;
+import com.google.api.services.monitoring.v3.model.MonitoredResource;
+import com.google.api.services.monitoring.v3.model.Point;
+import com.google.api.services.monitoring.v3.model.TimeSeries;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import google.registry.monitoring.metrics.CustomFitter;
+import google.registry.monitoring.metrics.Distribution;
+import google.registry.monitoring.metrics.ExponentialFitter;
+import google.registry.monitoring.metrics.LabelDescriptor;
+import google.registry.monitoring.metrics.LinearFitter;
+import google.registry.monitoring.metrics.Metric;
+import google.registry.monitoring.metrics.MetricPoint;
+import google.registry.monitoring.metrics.MetricSchema;
+import google.registry.monitoring.metrics.MetricSchema.Kind;
+import google.registry.monitoring.metrics.MutableDistribution;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.List;
+import org.joda.time.Instant;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+
+/** Unit tests for {@link StackdriverWriter}. */
+@RunWith(MockitoJUnitRunner.class)
+public class StackdriverWriterTest {
+
+ @Mock private Monitoring client;
+ @Mock private Monitoring.Projects projects;
+ @Mock private Monitoring.Projects.MetricDescriptors metricDescriptors;
+ @Mock private Monitoring.Projects.MetricDescriptors.Get metricDescriptorGet;
+ @Mock private Monitoring.Projects.TimeSeries timeSeries;
+ @Mock private Monitoring.Projects.MetricDescriptors.Create metricDescriptorCreate;
+ @Mock private Monitoring.Projects.TimeSeries.Create timeSeriesCreate;
+ @Mock private Metric metric;
+ @Mock private Metric boolMetric;
+ @Mock private Metric distributionMetric;
+ private static final String PROJECT = "PROJECT";
+ private static final int MAX_QPS = 10;
+ private static final int MAX_POINTS_PER_REQUEST = 10;
+ private static final MonitoredResource MONITORED_RESOURCE = new MonitoredResource();
+
+ @Before
+ public void setUp() throws Exception {
+ when(metric.getValueClass()).thenReturn(Long.class);
+ when(metric.getCardinality()).thenReturn(1);
+ when(metric.getMetricSchema())
+ .thenReturn(
+ MetricSchema.create(
+ "/name",
+ "desc",
+ "vdn",
+ Kind.CUMULATIVE,
+ ImmutableSet.of(LabelDescriptor.create("label1", "desc1"))));
+ // Store in an intermediate value, because Mockito hates when mocks are evaluated inside of
+ // thenReturn() methods.
+ MetricPoint longPoint =
+ MetricPoint.create(
+ metric, ImmutableList.of("value1"), new Instant(1337), new Instant(1338), 5L);
+ when(metric.getTimestampedValues()).thenReturn(ImmutableList.of(longPoint));
+
+ when(boolMetric.getValueClass()).thenReturn(Boolean.class);
+ when(boolMetric.getMetricSchema())
+ .thenReturn(
+ MetricSchema.create(
+ "/name",
+ "desc",
+ "vdn",
+ Kind.GAUGE,
+ ImmutableSet.of(LabelDescriptor.create("label1", "desc1"))));
+ // Store in an intermediate value, because Mockito hates when mocks are evaluated inside of
+ // thenReturn() methods.
+ MetricPoint boolPoint =
+ MetricPoint.create(boolMetric, ImmutableList.of("foo"), new Instant(1337), true);
+ when(boolMetric.getTimestampedValues()).thenReturn(ImmutableList.of(boolPoint));
+
+ when(distributionMetric.getMetricSchema())
+ .thenReturn(
+ MetricSchema.create(
+ "/name",
+ "desc",
+ "vdn",
+ Kind.GAUGE,
+ ImmutableSet.of(LabelDescriptor.create("label1", "desc1"))));
+ when(distributionMetric.getValueClass()).thenReturn(Distribution.class);
+
+ MetricDescriptor descriptor = StackdriverWriter.encodeMetricDescriptor(metric);
+ when(client.projects()).thenReturn(projects);
+ when(projects.metricDescriptors()).thenReturn(metricDescriptors);
+ when(projects.timeSeries()).thenReturn(timeSeries);
+ when(metricDescriptors.create(anyString(), any(MetricDescriptor.class)))
+ .thenReturn(metricDescriptorCreate);
+ when(metricDescriptorCreate.execute()).thenReturn(descriptor);
+ when(metricDescriptors.get(anyString())).thenReturn(metricDescriptorGet);
+ when(metricDescriptorGet.execute()).thenReturn(descriptor);
+ when(timeSeries.create(anyString(), any(CreateTimeSeriesRequest.class)))
+ .thenReturn(timeSeriesCreate);
+ }
+
+ @Test
+ public void testWrite_maxPoints_flushes() throws Exception {
+ StackdriverWriter writer =
+ spy(
+ new StackdriverWriter(
+ client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST));
+
+
+ for (int i = 0; i < MAX_POINTS_PER_REQUEST; i++) {
+ for (MetricPoint> point : metric.getTimestampedValues()) {
+ writer.write(point);
+ }
+ }
+
+ verify(writer).flush();
+ }
+
+ @Test
+ public void testWrite_lessThanMaxPoints_doesNotFlush() throws Exception {
+ StackdriverWriter writer =
+ spy(
+ new StackdriverWriter(
+ client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST));
+ for (int i = 0; i < MAX_POINTS_PER_REQUEST - 1; i++) {
+ for (MetricPoint> point : metric.getTimestampedValues()) {
+ writer.write(point);
+ }
+ }
+
+ verify(writer, never()).flush();
+ }
+
+ @Test
+ public void testWrite_invalidMetricType_throwsException() throws Exception {
+ when(metric.getValueClass())
+ .thenAnswer(
+ new Answer>() {
+ @Override
+ public Class> answer(InvocationOnMock invocation) throws Throwable {
+ return Object.class;
+ }
+ });
+ StackdriverWriter writer =
+ new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST);
+
+ for (MetricPoint> point : metric.getTimestampedValues()) {
+ try {
+ writer.write(point);
+ fail("expected IllegalArgumentException");
+ } catch (IOException expected) {}
+ }
+ }
+
+ @Test
+ public void testWrite_ManyPoints_flushesTwice() throws Exception {
+ StackdriverWriter writer =
+ spy(
+ new StackdriverWriter(
+ client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST));
+
+ for (int i = 0; i < MAX_POINTS_PER_REQUEST * 2; i++) {
+ for (MetricPoint> point : metric.getTimestampedValues()) {
+ writer.write(point);
+ }
+ }
+
+ verify(writer, times(2)).flush();
+ }
+
+ @Test
+ public void testRegisterMetric_registersWithStackdriver() throws Exception {
+ StackdriverWriter writer =
+ new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST);
+
+ writer.registerMetric(metric);
+
+ verify(
+ client
+ .projects()
+ .metricDescriptors()
+ .create(PROJECT, StackdriverWriter.encodeMetricDescriptor(metric)))
+ .execute();
+ }
+
+ @Test
+ public void registerMetric_doesNotReregisterDupe() throws Exception {
+ StackdriverWriter writer =
+ new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST);
+
+ writer.registerMetric(metric);
+ writer.registerMetric(metric);
+
+ verify(
+ client
+ .projects()
+ .metricDescriptors()
+ .create(PROJECT, StackdriverWriter.encodeMetricDescriptor(metric)))
+ .execute();
+ }
+
+ @Test
+ public void registerMetric_fetchesStackdriverDefinition() throws Exception {
+ // Stackdriver throws an Exception with the status message "ALREADY_EXISTS" when you try to
+ // register a metric that's already been registered, so we fake one here.
+ ByteArrayInputStream inputStream = new ByteArrayInputStream("".getBytes(UTF_8));
+ HttpResponse response = GoogleJsonResponseExceptionHelper.createHttpResponse(400, inputStream);
+ HttpResponseException.Builder httpResponseExceptionBuilder =
+ new HttpResponseException.Builder(response);
+ httpResponseExceptionBuilder.setStatusCode(400);
+ httpResponseExceptionBuilder.setStatusMessage("ALREADY_EXISTS");
+ GoogleJsonResponseException exception =
+ new GoogleJsonResponseException(httpResponseExceptionBuilder, null);
+ when(metricDescriptorCreate.execute()).thenThrow(exception);
+ StackdriverWriter writer =
+ new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST);
+
+ writer.registerMetric(metric);
+
+ verify(client.projects().metricDescriptors().get("metric")).execute();
+ }
+
+ @Test
+ public void registerMetric_rethrowsException() throws Exception {
+ ByteArrayInputStream inputStream = new ByteArrayInputStream("".getBytes(UTF_8));
+ HttpResponse response = GoogleJsonResponseExceptionHelper.createHttpResponse(400, inputStream);
+ HttpResponseException.Builder httpResponseExceptionBuilder =
+ new HttpResponseException.Builder(response);
+ httpResponseExceptionBuilder.setStatusCode(404);
+ GoogleJsonResponseException exception =
+ new GoogleJsonResponseException(httpResponseExceptionBuilder, null);
+ when(metricDescriptorCreate.execute()).thenThrow(exception);
+ StackdriverWriter writer =
+ new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST);
+
+ try {
+ writer.registerMetric(metric);
+ fail("Expected GoogleJsonResponseException");
+ } catch (GoogleJsonResponseException expected) {
+ assertThat(exception.getStatusCode()).isEqualTo(404);
+ }
+ }
+
+ @Test
+ public void getEncodedTimeSeries_nullLabels_encodes() throws Exception {
+ ByteArrayInputStream inputStream = new ByteArrayInputStream("".getBytes(UTF_8));
+ HttpResponse response = GoogleJsonResponseExceptionHelper.createHttpResponse(400, inputStream);
+ HttpResponseException.Builder httpResponseExceptionBuilder =
+ new HttpResponseException.Builder(response);
+ httpResponseExceptionBuilder.setStatusCode(400);
+ httpResponseExceptionBuilder.setStatusMessage("ALREADY_EXISTS");
+ GoogleJsonResponseException exception =
+ new GoogleJsonResponseException(httpResponseExceptionBuilder, null);
+ when(metricDescriptorCreate.execute()).thenThrow(exception);
+ when(metricDescriptorGet.execute())
+ .thenReturn(new MetricDescriptor().setName("foo").setLabels(null));
+ StackdriverWriter writer =
+ new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST);
+ writer.registerMetric(metric);
+
+ TimeSeries timeSeries =
+ writer.getEncodedTimeSeries(
+ MetricPoint.create(metric, ImmutableList.of("foo"), new Instant(1337), 10L));
+
+ assertThat(timeSeries.getMetric().getLabels()).isEmpty();
+ }
+
+ @Test
+ public void encodeMetricDescriptor_simpleMetric_encodes() {
+ MetricDescriptor descriptor = StackdriverWriter.encodeMetricDescriptor(metric);
+
+ assertThat(descriptor.getType()).isEqualTo("custom.googleapis.com/name");
+ assertThat(descriptor.getValueType()).isEqualTo("INT64");
+ assertThat(descriptor.getDescription()).isEqualTo("desc");
+ assertThat(descriptor.getDisplayName()).isEqualTo("vdn");
+ assertThat(descriptor.getLabels())
+ .containsExactly(
+ new com.google.api.services.monitoring.v3.model.LabelDescriptor()
+ .setValueType("STRING")
+ .setKey("label1")
+ .setDescription("desc1"));
+ }
+
+ @Test
+ public void encodeLabelDescriptors_simpleLabels_encodes() {
+ ImmutableSet descriptors =
+ ImmutableSet.of(
+ LabelDescriptor.create("label1", "description1"),
+ LabelDescriptor.create("label2", "description2"));
+
+ ImmutableList encodedDescritors =
+ StackdriverWriter.encodeLabelDescriptors(descriptors);
+
+ assertThat(encodedDescritors)
+ .containsExactly(
+ new com.google.api.services.monitoring.v3.model.LabelDescriptor()
+ .setValueType("STRING")
+ .setKey("label1")
+ .setDescription("description1"),
+ new com.google.api.services.monitoring.v3.model.LabelDescriptor()
+ .setValueType("STRING")
+ .setKey("label2")
+ .setDescription("description2"));
+ }
+
+ @Test
+ public void getEncodedTimeSeries_cumulativeMetricPoint_ZeroInterval_encodesGreaterEndTime()
+ throws Exception {
+ StackdriverWriter writer =
+ new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST);
+ MetricPoint nativePoint =
+ MetricPoint.create(
+ metric, ImmutableList.of("foo"), new Instant(1337), new Instant(1337), 10L);
+
+ TimeSeries timeSeries = writer.getEncodedTimeSeries(nativePoint);
+
+ assertThat(timeSeries.getValueType()).isEqualTo("INT64");
+ assertThat(timeSeries.getMetricKind()).isEqualTo("CUMULATIVE");
+ List points = timeSeries.getPoints();
+ assertThat(points).hasSize(1);
+ Point point = points.get(0);
+ assertThat(point.getValue().getInt64Value()).isEqualTo(10L);
+ assertThat(point.getInterval().getStartTime()).isEqualTo("1970-01-01T00:00:01.337Z");
+ assertThat(point.getInterval().getEndTime()).isEqualTo("1970-01-01T00:00:01.338Z");
+ }
+
+ @Test
+ public void getEncodedTimeSeries_cumulativeMetricPoint_nonZeroInterval_encodesSameInterval()
+ throws Exception {
+ StackdriverWriter writer =
+ new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST);
+ MetricPoint nativePoint =
+ MetricPoint.create(
+ metric, ImmutableList.of("foo"), new Instant(1337), new Instant(1339), 10L);
+
+ TimeSeries timeSeries = writer.getEncodedTimeSeries(nativePoint);
+
+ assertThat(timeSeries.getValueType()).isEqualTo("INT64");
+ assertThat(timeSeries.getMetricKind()).isEqualTo("CUMULATIVE");
+ List points = timeSeries.getPoints();
+ assertThat(points).hasSize(1);
+ Point point = points.get(0);
+ assertThat(point.getValue().getInt64Value()).isEqualTo(10L);
+ assertThat(point.getInterval().getStartTime()).isEqualTo("1970-01-01T00:00:01.337Z");
+ assertThat(point.getInterval().getEndTime()).isEqualTo("1970-01-01T00:00:01.339Z");
+ }
+
+ @Test
+ public void getEncodedTimeSeries_gaugeMetricPoint_zeroInterval_encodesSameInterval()
+ throws Exception {
+ when(metric.getMetricSchema())
+ .thenReturn(
+ MetricSchema.create(
+ "/name",
+ "desc",
+ "vdn",
+ Kind.GAUGE,
+ ImmutableSet.of(LabelDescriptor.create("label1", "desc1"))));
+ // Store in an intermediate value, because Mockito hates when mocks are evaluated inside of
+ // thenReturn() methods.
+ MetricPoint testPoint =
+ MetricPoint.create(metric, ImmutableList.of("foo"), new Instant(1337), 10L);
+ when(metric.getTimestampedValues()).thenReturn(ImmutableList.of(testPoint));
+ // Store in an intermediate value, because Mockito hates when mocks are evaluated inside of
+ // thenReturn() methods.
+ MetricDescriptor descriptor = StackdriverWriter.encodeMetricDescriptor(metric);
+ when(metricDescriptorCreate.execute()).thenReturn(descriptor);
+ StackdriverWriter writer =
+ new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST);
+ MetricPoint nativePoint =
+ MetricPoint.create(
+ metric, ImmutableList.of("foo"), new Instant(1337), new Instant(1337), 10L);
+
+ TimeSeries timeSeries = writer.getEncodedTimeSeries(nativePoint);
+
+ assertThat(timeSeries.getValueType()).isEqualTo("INT64");
+ assertThat(timeSeries.getMetricKind()).isEqualTo("GAUGE");
+ List points = timeSeries.getPoints();
+ assertThat(points).hasSize(1);
+ Point point = points.get(0);
+ assertThat(point.getValue().getInt64Value()).isEqualTo(10L);
+ assertThat(point.getInterval().getStartTime()).isEqualTo("1970-01-01T00:00:01.337Z");
+ assertThat(point.getInterval().getEndTime()).isEqualTo("1970-01-01T00:00:01.337Z");
+ }
+
+ @Test
+ public void getEncodedTimeSeries_booleanMetric_encodes() throws Exception {
+ StackdriverWriter writer =
+ new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST);
+
+ MetricDescriptor boolDescriptor = StackdriverWriter.encodeMetricDescriptor(boolMetric);
+ when(metricDescriptorCreate.execute()).thenReturn(boolDescriptor);
+ MetricPoint nativePoint =
+ MetricPoint.create(boolMetric, ImmutableList.of("foo"), new Instant(1337), true);
+
+ TimeSeries timeSeries = writer.getEncodedTimeSeries(nativePoint);
+
+ assertThat(timeSeries.getValueType()).isEqualTo("BOOL");
+ assertThat(timeSeries.getMetricKind()).isEqualTo("GAUGE");
+ List points = timeSeries.getPoints();
+ assertThat(points).hasSize(1);
+ Point point = points.get(0);
+ assertThat(point.getValue().getBoolValue()).isEqualTo(true);
+ assertThat(point.getInterval().getEndTime()).isEqualTo("1970-01-01T00:00:01.337Z");
+ assertThat(point.getInterval().getStartTime()).isEqualTo("1970-01-01T00:00:01.337Z");
+ }
+
+ @Test
+ public void getEncodedTimeSeries_distributionMetricCustomFitter_encodes() throws Exception {
+ StackdriverWriter writer =
+ new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST);
+
+ MetricDescriptor descriptor = StackdriverWriter.encodeMetricDescriptor(distributionMetric);
+ when(metricDescriptorCreate.execute()).thenReturn(descriptor);
+ MutableDistribution distribution =
+ new MutableDistribution(CustomFitter.create(ImmutableSet.of(5.0)));
+ distribution.add(10.0, 5L);
+ distribution.add(0.0, 5L);
+ MetricPoint nativePoint =
+ MetricPoint.create(
+ distributionMetric, ImmutableList.of("foo"), new Instant(1337), distribution);
+
+ TimeSeries timeSeries = writer.getEncodedTimeSeries(nativePoint);
+
+ assertThat(timeSeries.getValueType()).isEqualTo("DISTRIBUTION");
+ assertThat(timeSeries.getMetricKind()).isEqualTo("GAUGE");
+ List points = timeSeries.getPoints();
+ assertThat(points).hasSize(1);
+ Point point = points.get(0);
+ assertThat(point.getValue().getDistributionValue())
+ .isEqualTo(
+ new com.google.api.services.monitoring.v3.model.Distribution()
+ .setMean(5.0)
+ .setSumOfSquaredDeviation(250.0)
+ .setCount(10L)
+ .setBucketCounts(ImmutableList.of(5L, 5L))
+ .setBucketOptions(
+ new BucketOptions()
+ .setExplicitBuckets(new Explicit().setBounds(ImmutableList.of(5.0)))));
+ assertThat(point.getInterval().getEndTime()).isEqualTo("1970-01-01T00:00:01.337Z");
+ assertThat(point.getInterval().getStartTime()).isEqualTo("1970-01-01T00:00:01.337Z");
+ }
+
+ @Test
+ public void getEncodedTimeSeries_distributionMetricLinearFitter_encodes() throws Exception {
+ StackdriverWriter writer =
+ new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST);
+
+ MetricDescriptor descriptor = StackdriverWriter.encodeMetricDescriptor(distributionMetric);
+ when(metricDescriptorCreate.execute()).thenReturn(descriptor);
+ MutableDistribution distribution = new MutableDistribution(LinearFitter.create(2, 5.0, 3.0));
+ distribution.add(0.0, 1L);
+ distribution.add(3.0, 2L);
+ distribution.add(10.0, 5L);
+ distribution.add(20.0, 5L);
+ MetricPoint nativePoint =
+ MetricPoint.create(
+ distributionMetric, ImmutableList.of("foo"), new Instant(1337), distribution);
+
+
+ TimeSeries timeSeries = writer.getEncodedTimeSeries(nativePoint);
+
+ assertThat(timeSeries.getValueType()).isEqualTo("DISTRIBUTION");
+ assertThat(timeSeries.getMetricKind()).isEqualTo("GAUGE");
+ List points = timeSeries.getPoints();
+ assertThat(points).hasSize(1);
+ Point point = points.get(0);
+ assertThat(point.getValue().getDistributionValue())
+ .isEqualTo(
+ new com.google.api.services.monitoring.v3.model.Distribution()
+ .setMean(12.0)
+ .setSumOfSquaredDeviation(646.0)
+ .setCount(13L)
+ .setBucketCounts(ImmutableList.of(1L, 2L, 5L, 5L))
+ .setBucketOptions(
+ new BucketOptions()
+ .setLinearBuckets(
+ new Linear().setNumFiniteBuckets(2).setWidth(5.0).setOffset(3.0))));
+ assertThat(point.getInterval().getEndTime()).isEqualTo("1970-01-01T00:00:01.337Z");
+ assertThat(point.getInterval().getStartTime()).isEqualTo("1970-01-01T00:00:01.337Z");
+ }
+
+ @Test
+ public void getEncodedTimeSeries_distributionMetricExponentialFitter_encodes() throws Exception {
+ StackdriverWriter writer =
+ new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST);
+
+ MetricDescriptor descriptor = StackdriverWriter.encodeMetricDescriptor(distributionMetric);
+ when(metricDescriptorCreate.execute()).thenReturn(descriptor);
+ MutableDistribution distribution =
+ new MutableDistribution(ExponentialFitter.create(2, 3.0, 0.5));
+ distribution.add(0.0, 1L);
+ distribution.add(3.0, 2L);
+ distribution.add(10.0, 5L);
+ distribution.add(20.0, 5L);
+ MetricPoint nativePoint =
+ MetricPoint.create(
+ distributionMetric, ImmutableList.of("foo"), new Instant(1337), distribution);
+
+ TimeSeries timeSeries = writer.getEncodedTimeSeries(nativePoint);
+
+ assertThat(timeSeries.getValueType()).isEqualTo("DISTRIBUTION");
+ assertThat(timeSeries.getMetricKind()).isEqualTo("GAUGE");
+ List points = timeSeries.getPoints();
+ assertThat(points).hasSize(1);
+ Point point = points.get(0);
+ assertThat(point.getValue().getDistributionValue())
+ .isEqualTo(
+ new com.google.api.services.monitoring.v3.model.Distribution()
+ .setMean(12.0)
+ .setSumOfSquaredDeviation(646.0)
+ .setCount(13L)
+ .setBucketCounts(ImmutableList.of(1L, 0L, 2L, 10L))
+ .setBucketOptions(
+ new BucketOptions()
+ .setExponentialBuckets(
+ new Exponential()
+ .setNumFiniteBuckets(2)
+ .setGrowthFactor(3.0)
+ .setScale(0.5))));
+ assertThat(point.getInterval().getEndTime()).isEqualTo("1970-01-01T00:00:01.337Z");
+ assertThat(point.getInterval().getStartTime()).isEqualTo("1970-01-01T00:00:01.337Z");
+ }
+}