diff options
Diffstat (limited to 'exporters/trace/stackdriver')
10 files changed, 1726 insertions, 0 deletions
diff --git a/exporters/trace/stackdriver/README.md b/exporters/trace/stackdriver/README.md new file mode 100644 index 00000000..9186a47c --- /dev/null +++ b/exporters/trace/stackdriver/README.md @@ -0,0 +1,127 @@ +# OpenCensus Stackdriver Trace Exporter +[![Build Status][travis-image]][travis-url] +[![Windows Build Status][appveyor-image]][appveyor-url] +[![Maven Central][maven-image]][maven-url] + +The *OpenCensus Stackdriver Trace Exporter* is a trace exporter that exports data to +Stackdriver Trace. [Stackdriver Trace][stackdriver-trace] is a distributed +tracing system that collects latency data from your applications and displays it in the Google +Cloud Platform Console. You can track how requests propagate through your application and receive +detailed near real-time performance insights. + +## Quickstart + +### Prerequisites + +To use this exporter, you must have an application that you'd like to trace. The app can be on +Google Cloud Platform, on-premise, or another cloud platform. + +In order to be able to push your traces to [Stackdriver Trace][stackdriver-trace], you must: + +1. [Create a Cloud project](https://support.google.com/cloud/answer/6251787?hl=en). +2. [Enable billing](https://support.google.com/cloud/answer/6288653#new-billing). +3. [Enable the Stackdriver Trace API](https://console.cloud.google.com/apis/api/cloudtrace.googleapis.com/overview). + +These steps enable the API but don't require that your app is hosted on Google Cloud Platform. + +### Hello "Stackdriver Trace" + +#### Add the dependencies to your project + +For Maven add to your `pom.xml`: +```xml +<dependencies> + <dependency> + <groupId>io.opencensus</groupId> + <artifactId>opencensus-api</artifactId> + <version>0.16.1</version> + </dependency> + <dependency> + <groupId>io.opencensus</groupId> + <artifactId>opencensus-exporter-trace-stackdriver</artifactId> + <version>0.16.1</version> + </dependency> + <dependency> + <groupId>io.opencensus</groupId> + <artifactId>opencensus-impl</artifactId> + <version>0.16.1</version> + <scope>runtime</scope> + </dependency> +</dependencies> +``` + +For Gradle add to your dependencies: +```groovy +compile 'io.opencensus:opencensus-api:0.16.1' +compile 'io.opencensus:opencensus-exporter-trace-stackdriver:0.16.1' +runtime 'io.opencensus:opencensus-impl:0.16.1' +``` + +#### Register the exporter + +This uses the default configuration for authentication and project ID. + +```java +public class MyMainClass { + public static void main(String[] args) throws Exception { + StackdriverTraceExporter.createAndRegister( + StackdriverTraceConfiguration.builder().build()); + // ... + } +} +``` + +#### Authentication + +This exporter uses [google-cloud-java](https://github.com/GoogleCloudPlatform/google-cloud-java), +for details about how to configure the authentication see [here](https://github.com/GoogleCloudPlatform/google-cloud-java#authentication). + +If you prefer to manually set the credentials use: +``` +StackdriverTraceExporter.createAndRegisterWithCredentialsAndProjectId( + new GoogleCredentials(new AccessToken(accessToken, expirationTime)), + "MyStackdriverProjectId"); +``` + +#### Specifying a Project ID + +This exporter uses [google-cloud-java](https://github.com/GoogleCloudPlatform/google-cloud-java), +for details about how to configure the project ID see [here](https://github.com/GoogleCloudPlatform/google-cloud-java#specifying-a-project-id). + +If you prefer to manually set the project ID use: +``` +StackdriverTraceExporter.createAndRegisterWithProjectId("MyStackdriverProjectId"); +``` + +#### Enable Stackdriver Trace API access scope on Google Cloud Platform +If your Stackdriver Trace Exporter is running on Kubernetes Engine or Compute Engine, +you might need additional setup to explicitly enable the ```trace.append``` Stackdriver +Trace API access scope. To do that, please follow the instructions for +[GKE](https://cloud.google.com/trace/docs/setup/java#kubernetes_engine) or +[GCE](https://cloud.google.com/trace/docs/setup/java#compute_engine). + +#### Java Versions + +Java 7 or above is required for using this exporter. + +## FAQ +### Why do I not see some trace events in Stackdriver? +In all the versions before '0.9.1' the Stackdriver Trace exporter was implemented using the [v1 +API][stackdriver-v1-api-url] which is not fully compatible with the OpenCensus data model. Trace +events like Annotations and NetworkEvents will be dropped. + +### Why do I get a "StatusRuntimeException: NOT_FOUND: Requested entity was not found"? +One of the possible reasons is you are using a project id with bad format for the exporter. +Please double check the project id associated with the Stackdriver Trace exporter first. +Stackdriver Trace backend will not do any sanitization or trimming on the incoming project id. +Project id with leading or trailing spaces will be treated as a separate non-existing project +(e.g "project-id" vs "project-id "), and will cause a NOT_FOUND exception. + +[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master +[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java +[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true +[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master +[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-stackdriver/badge.svg +[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-stackdriver +[stackdriver-trace]: https://cloud.google.com/trace/ +[stackdriver-v1-api-url]: https://cloud.google.com/trace/docs/reference/v1/rpc/google.devtools.cloudtrace.v1#google.devtools.cloudtrace.v1.TraceSpan diff --git a/exporters/trace/stackdriver/build.gradle b/exporters/trace/stackdriver/build.gradle new file mode 100644 index 00000000..83dc970e --- /dev/null +++ b/exporters/trace/stackdriver/build.gradle @@ -0,0 +1,31 @@ +description = 'OpenCensus Trace Stackdriver Exporter' + +[compileJava, compileTestJava].each() { + it.sourceCompatibility = 1.7 + it.targetCompatibility = 1.7 +} + +dependencies { + compileOnly libraries.auto_value + + compile project(':opencensus-api'), + project(':opencensus-contrib-monitored-resource-util'), + libraries.google_auth, + libraries.guava + + compile (libraries.google_cloud_trace) { + // Prefer library version. + exclude group: 'com.google.guava', module: 'guava' + + // Prefer library version. + exclude group: 'com.google.code.findbugs', module: 'jsr305' + + // We will always be more up to date. + exclude group: 'io.opencensus', module: 'opencensus-api' + } + + testCompile project(':opencensus-api') + + signature "org.codehaus.mojo.signature:java17:1.0@signature" + signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature" +} diff --git a/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverExporter.java b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverExporter.java new file mode 100644 index 00000000..8797cc77 --- /dev/null +++ b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverExporter.java @@ -0,0 +1,148 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.exporter.trace.stackdriver; + +import com.google.auth.Credentials; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.ServiceOptions; +import com.google.common.annotations.VisibleForTesting; +import io.opencensus.trace.export.SpanExporter; +import io.opencensus.trace.export.SpanExporter.Handler; +import java.io.IOException; + +/** + * An OpenCensus span exporter implementation which exports data to Stackdriver Trace. + * + * <p>Example of usage on Google Cloud VMs: + * + * <pre>{@code + * public static void main(String[] args) { + * StackdriverExporter.createAndRegisterWithProjectId("MyStackdriverProjectId"); + * ... // Do work. + * } + * }</pre> + * + * @deprecated Deprecated due to inconsistent naming. Use {@link StackdriverTraceExporter}. + * @since 0.6 + */ +@Deprecated +public final class StackdriverExporter { + + /** + * Creates and registers the Stackdriver Trace exporter to the OpenCensus library for an explicit + * project ID and using explicit credentials. Only one Stackdriver exporter can be registered at + * any point. + * + * @param credentials a credentials used to authenticate API calls. + * @param projectId the cloud project id. + * @throws IllegalStateException if a Stackdriver exporter is already registered. + * @since 0.6 + */ + public static void createAndRegisterWithCredentialsAndProjectId( + Credentials credentials, String projectId) throws IOException { + StackdriverTraceExporter.createAndRegister( + StackdriverTraceConfiguration.builder() + .setCredentials(credentials) + .setProjectId(projectId) + .build()); + } + + /** + * Creates and registers the Stackdriver Trace exporter to the OpenCensus library for an explicit + * project ID. Only one Stackdriver exporter can be registered at any point. + * + * <p>This uses the default application credentials see {@link + * GoogleCredentials#getApplicationDefault}. + * + * <p>This is equivalent with: + * + * <pre>{@code + * StackdriverExporter.createAndRegisterWithCredentialsAndProjectId( + * GoogleCredentials.getApplicationDefault(), projectId); + * }</pre> + * + * @param projectId the cloud project id. + * @throws IllegalStateException if a Stackdriver exporter is already registered. + * @since 0.6 + */ + public static void createAndRegisterWithProjectId(String projectId) throws IOException { + StackdriverTraceExporter.createAndRegister( + StackdriverTraceConfiguration.builder() + .setCredentials(GoogleCredentials.getApplicationDefault()) + .setProjectId(projectId) + .build()); + } + + /** + * Creates and registers the Stackdriver Trace exporter to the OpenCensus library. Only one + * Stackdriver exporter can be registered at any point. + * + * <p>This uses the default application credentials see {@link + * GoogleCredentials#getApplicationDefault}. + * + * <p>This uses the default project ID configured see {@link ServiceOptions#getDefaultProjectId}. + * + * <p>This is equivalent with: + * + * <pre>{@code + * StackdriverExporter.createAndRegisterWithProjectId(ServiceOptions.getDefaultProjectId()); + * }</pre> + * + * @throws IllegalStateException if a Stackdriver exporter is already registered. + * @since 0.6 + */ + public static void createAndRegister() throws IOException { + StackdriverTraceExporter.createAndRegister( + StackdriverTraceConfiguration.builder() + .setCredentials(GoogleCredentials.getApplicationDefault()) + .setProjectId(ServiceOptions.getDefaultProjectId()) + .build()); + } + + /** + * Registers the {@code StackdriverExporter}. + * + * @param spanExporter the instance of the {@code SpanExporter} where this service is registered. + */ + @VisibleForTesting + static void register(SpanExporter spanExporter, Handler handler) { + StackdriverTraceExporter.register(spanExporter, handler); + } + + /** + * Unregisters the Stackdriver Trace exporter from the OpenCensus library. + * + * @throws IllegalStateException if a Stackdriver exporter is not registered. + * @since 0.6 + */ + public static void unregister() { + StackdriverTraceExporter.unregister(); + } + + /** + * Unregisters the {@code StackdriverExporter}. + * + * @param spanExporter the instance of the {@code SpanExporter} from where this service is + * unregistered. + */ + @VisibleForTesting + static void unregister(SpanExporter spanExporter) { + StackdriverTraceExporter.unregister(spanExporter); + } + + private StackdriverExporter() {} +} diff --git a/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfiguration.java b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfiguration.java new file mode 100644 index 00000000..f78832d0 --- /dev/null +++ b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfiguration.java @@ -0,0 +1,118 @@ +/* + * Copyright 2018, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.exporter.trace.stackdriver; + +import com.google.auth.Credentials; +import com.google.auto.value.AutoValue; +import com.google.cloud.trace.v2.stub.TraceServiceStub; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Configurations for {@link StackdriverTraceExporter}. + * + * @since 0.12 + */ +@AutoValue +@Immutable +public abstract class StackdriverTraceConfiguration { + + StackdriverTraceConfiguration() {} + + /** + * Returns the {@link Credentials}. + * + * @return the {@code Credentials}. + * @since 0.12 + */ + @Nullable + public abstract Credentials getCredentials(); + + /** + * Returns the cloud project id. + * + * @return the cloud project id. + * @since 0.12 + */ + @Nullable + public abstract String getProjectId(); + + /** + * Returns a TraceServiceStub instance used to make RPC calls. + * + * @return the trace service stub. + * @since 0.16 + */ + @Nullable + public abstract TraceServiceStub getTraceServiceStub(); + + /** + * Returns a new {@link Builder}. + * + * @return a {@code Builder}. + * @since 0.12 + */ + public static Builder builder() { + return new AutoValue_StackdriverTraceConfiguration.Builder(); + } + + /** + * Builder for {@link StackdriverTraceConfiguration}. + * + * @since 0.12 + */ + @AutoValue.Builder + public abstract static class Builder { + + Builder() {} + + /** + * Sets the {@link Credentials} used to authenticate API calls. + * + * @param credentials the {@code Credentials}. + * @return this. + * @since 0.12 + */ + public abstract Builder setCredentials(Credentials credentials); + + /** + * Sets the cloud project id. + * + * @param projectId the cloud project id. + * @return this. + * @since 0.12 + */ + public abstract Builder setProjectId(String projectId); + + /** + * Sets the trace service stub used to send gRPC calls. + * + * @param traceServiceStub the {@code TraceServiceStub}. + * @return this. + * @since 0.16 + */ + public abstract Builder setTraceServiceStub(TraceServiceStub traceServiceStub); + + /** + * Builds a {@link StackdriverTraceConfiguration}. + * + * @return a {@code StackdriverTraceConfiguration}. + * @since 0.12 + */ + public abstract StackdriverTraceConfiguration build(); + } +} diff --git a/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporter.java b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporter.java new file mode 100644 index 00000000..0182ae94 --- /dev/null +++ b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporter.java @@ -0,0 +1,141 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.exporter.trace.stackdriver; + +import static com.google.common.base.Preconditions.checkState; + +import com.google.auth.Credentials; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.ServiceOptions; +import com.google.cloud.trace.v2.TraceServiceClient; +import com.google.cloud.trace.v2.stub.TraceServiceStub; +import com.google.common.annotations.VisibleForTesting; +import io.opencensus.trace.Tracing; +import io.opencensus.trace.export.SpanExporter; +import io.opencensus.trace.export.SpanExporter.Handler; +import java.io.IOException; +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; + +/** + * An OpenCensus span exporter implementation which exports data to Stackdriver Trace. + * + * <p>Example of usage on Google Cloud VMs: + * + * <pre>{@code + * public static void main(String[] args) { + * StackdriverTraceExporter.createAndRegister( + * StackdriverTraceConfiguration.builder() + * .setProjectId("MyStackdriverProjectId") + * .build()); + * ... // Do work. + * } + * }</pre> + * + * @since 0.12 + */ +public final class StackdriverTraceExporter { + + private static final String REGISTER_NAME = StackdriverTraceExporter.class.getName(); + private static final Object monitor = new Object(); + + @GuardedBy("monitor") + @Nullable + private static Handler handler = null; + + /** + * Creates and registers the Stackdriver Trace exporter to the OpenCensus library. Only one + * Stackdriver exporter can be registered at any point. + * + * <p>If the {@code credentials} in the provided {@link StackdriverTraceConfiguration} is not set, + * the exporter will use the default application credentials. See {@link + * GoogleCredentials#getApplicationDefault}. + * + * <p>If the {@code projectId} in the provided {@link StackdriverTraceConfiguration} is not set, + * the exporter will use the default project ID. See {@link ServiceOptions#getDefaultProjectId}. + * + * @param configuration the {@code StackdriverTraceConfiguration} used to create the exporter. + * @throws IllegalStateException if a Stackdriver exporter is already registered. + * @since 0.12 + */ + public static void createAndRegister(StackdriverTraceConfiguration configuration) + throws IOException { + synchronized (monitor) { + checkState(handler == null, "Stackdriver exporter is already registered."); + Credentials credentials = configuration.getCredentials(); + String projectId = configuration.getProjectId(); + projectId = projectId != null ? projectId : ServiceOptions.getDefaultProjectId(); + + StackdriverV2ExporterHandler handler; + TraceServiceStub stub = configuration.getTraceServiceStub(); + if (stub == null) { + handler = + StackdriverV2ExporterHandler.createWithCredentials( + credentials != null ? credentials : GoogleCredentials.getApplicationDefault(), + projectId); + } else { + handler = new StackdriverV2ExporterHandler(projectId, TraceServiceClient.create(stub)); + } + + registerInternal(handler); + } + } + + private static void registerInternal(Handler newHandler) { + synchronized (monitor) { + handler = newHandler; + register(Tracing.getExportComponent().getSpanExporter(), newHandler); + } + } + + /** + * Registers the {@code StackdriverTraceExporter}. + * + * @param spanExporter the instance of the {@code SpanExporter} where this service is registered. + */ + @VisibleForTesting + static void register(SpanExporter spanExporter, Handler handler) { + spanExporter.registerHandler(REGISTER_NAME, handler); + } + + /** + * Unregisters the Stackdriver Trace exporter from the OpenCensus library. + * + * @throws IllegalStateException if a Stackdriver exporter is not registered. + * @since 0.12 + */ + public static void unregister() { + synchronized (monitor) { + checkState(handler != null, "Stackdriver exporter is not registered."); + unregister(Tracing.getExportComponent().getSpanExporter()); + handler = null; + } + } + + /** + * Unregisters the {@code StackdriverTraceExporter}. + * + * @param spanExporter the instance of the {@code SpanExporter} from where this service is + * unregistered. + */ + @VisibleForTesting + static void unregister(SpanExporter spanExporter) { + spanExporter.unregisterHandler(REGISTER_NAME); + } + + private StackdriverTraceExporter() {} +} diff --git a/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandler.java b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandler.java new file mode 100644 index 00000000..de022c3f --- /dev/null +++ b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandler.java @@ -0,0 +1,501 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.exporter.trace.stackdriver; + +import static com.google.api.client.util.Preconditions.checkNotNull; + +import com.google.api.gax.core.FixedCredentialsProvider; +import com.google.auth.Credentials; +import com.google.cloud.trace.v2.TraceServiceClient; +import com.google.cloud.trace.v2.TraceServiceSettings; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import com.google.devtools.cloudtrace.v2.AttributeValue; +import com.google.devtools.cloudtrace.v2.AttributeValue.Builder; +import com.google.devtools.cloudtrace.v2.ProjectName; +import com.google.devtools.cloudtrace.v2.Span; +import com.google.devtools.cloudtrace.v2.Span.Attributes; +import com.google.devtools.cloudtrace.v2.Span.Link; +import com.google.devtools.cloudtrace.v2.Span.Links; +import com.google.devtools.cloudtrace.v2.Span.TimeEvent; +import com.google.devtools.cloudtrace.v2.Span.TimeEvent.MessageEvent; +import com.google.devtools.cloudtrace.v2.SpanName; +import com.google.devtools.cloudtrace.v2.TruncatableString; +import com.google.protobuf.Int32Value; +import com.google.rpc.Status; +import io.opencensus.common.Function; +import io.opencensus.common.Functions; +import io.opencensus.common.OpenCensusLibraryInformation; +import io.opencensus.common.Scope; +import io.opencensus.common.Timestamp; +import io.opencensus.contrib.monitoredresource.util.MonitoredResource; +import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource; +import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource; +import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource; +import io.opencensus.contrib.monitoredresource.util.MonitoredResourceUtils; +import io.opencensus.contrib.monitoredresource.util.ResourceType; +import io.opencensus.trace.Annotation; +import io.opencensus.trace.MessageEvent.Type; +import io.opencensus.trace.Sampler; +import io.opencensus.trace.Span.Kind; +import io.opencensus.trace.SpanContext; +import io.opencensus.trace.Tracer; +import io.opencensus.trace.Tracing; +import io.opencensus.trace.export.SpanData; +import io.opencensus.trace.export.SpanData.TimedEvent; +import io.opencensus.trace.export.SpanData.TimedEvents; +import io.opencensus.trace.export.SpanExporter; +import io.opencensus.trace.samplers.Samplers; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/*>>> +import org.checkerframework.checker.nullness.qual.Nullable; +*/ + +/** Exporter to Stackdriver Trace API v2. */ +final class StackdriverV2ExporterHandler extends SpanExporter.Handler { + + private static final Tracer tracer = Tracing.getTracer(); + private static final Sampler probabilitySampler = Samplers.probabilitySampler(0.0001); + private static final String AGENT_LABEL_KEY = "g.co/agent"; + private static final String AGENT_LABEL_VALUE_STRING = + "opencensus-java [" + OpenCensusLibraryInformation.VERSION + "]"; + private static final String SERVER_PREFIX = "Recv."; + private static final String CLIENT_PREFIX = "Sent."; + private static final AttributeValue AGENT_LABEL_VALUE = + AttributeValue.newBuilder() + .setStringValue(toTruncatableStringProto(AGENT_LABEL_VALUE_STRING)) + .build(); + + private static final ImmutableMap<String, String> HTTP_ATTRIBUTE_MAPPING = + ImmutableMap.<String, String>builder() + .put("http.host", "/http/host") + .put("http.method", "/http/method") + .put("http.path", "/http/path") + .put("http.route", "/http/route") + .put("http.user_agent", "/http/user_agent") + .put("http.status_code", "/http/status_code") + .build(); + + @javax.annotation.Nullable + private static final MonitoredResource RESOURCE = MonitoredResourceUtils.getDefaultResource(); + + // Only initialize once. + private static final Map<String, AttributeValue> RESOURCE_LABELS = getResourceLabels(RESOURCE); + + // Constant functions for AttributeValue. + private static final Function<String, /*@Nullable*/ AttributeValue> stringAttributeValueFunction = + new Function<String, /*@Nullable*/ AttributeValue>() { + @Override + public AttributeValue apply(String stringValue) { + Builder attributeValueBuilder = AttributeValue.newBuilder(); + attributeValueBuilder.setStringValue(toTruncatableStringProto(stringValue)); + return attributeValueBuilder.build(); + } + }; + private static final Function<Boolean, /*@Nullable*/ AttributeValue> + booleanAttributeValueFunction = + new Function<Boolean, /*@Nullable*/ AttributeValue>() { + @Override + public AttributeValue apply(Boolean booleanValue) { + Builder attributeValueBuilder = AttributeValue.newBuilder(); + attributeValueBuilder.setBoolValue(booleanValue); + return attributeValueBuilder.build(); + } + }; + private static final Function<Long, /*@Nullable*/ AttributeValue> longAttributeValueFunction = + new Function<Long, /*@Nullable*/ AttributeValue>() { + @Override + public AttributeValue apply(Long longValue) { + Builder attributeValueBuilder = AttributeValue.newBuilder(); + attributeValueBuilder.setIntValue(longValue); + return attributeValueBuilder.build(); + } + }; + private static final Function<Double, /*@Nullable*/ AttributeValue> doubleAttributeValueFunction = + new Function<Double, /*@Nullable*/ AttributeValue>() { + @Override + public AttributeValue apply(Double doubleValue) { + Builder attributeValueBuilder = AttributeValue.newBuilder(); + // TODO: set double value if Stackdriver Trace support it in the future. + attributeValueBuilder.setStringValue( + toTruncatableStringProto(String.valueOf(doubleValue))); + return attributeValueBuilder.build(); + } + }; + + private final String projectId; + private final TraceServiceClient traceServiceClient; + private final ProjectName projectName; + + @VisibleForTesting + StackdriverV2ExporterHandler(String projectId, TraceServiceClient traceServiceClient) { + this.projectId = checkNotNull(projectId, "projectId"); + this.traceServiceClient = traceServiceClient; + projectName = ProjectName.of(this.projectId); + + Tracing.getExportComponent() + .getSampledSpanStore() + .registerSpanNamesForCollection(Collections.singletonList("ExportStackdriverTraces")); + } + + static StackdriverV2ExporterHandler createWithCredentials( + Credentials credentials, String projectId) throws IOException { + checkNotNull(credentials, "credentials"); + TraceServiceSettings traceServiceSettings = + TraceServiceSettings.newBuilder() + .setCredentialsProvider(FixedCredentialsProvider.create(credentials)) + .build(); + return new StackdriverV2ExporterHandler( + projectId, TraceServiceClient.create(traceServiceSettings)); + } + + @VisibleForTesting + Span generateSpan(SpanData spanData, Map<String, AttributeValue> resourceLabels) { + SpanContext context = spanData.getContext(); + final String spanIdHex = context.getSpanId().toLowerBase16(); + SpanName spanName = + SpanName.newBuilder() + .setProject(projectId) + .setTrace(context.getTraceId().toLowerBase16()) + .setSpan(spanIdHex) + .build(); + Span.Builder spanBuilder = + Span.newBuilder() + .setName(spanName.toString()) + .setSpanId(spanIdHex) + .setDisplayName( + toTruncatableStringProto(toDisplayName(spanData.getName(), spanData.getKind()))) + .setStartTime(toTimestampProto(spanData.getStartTimestamp())) + .setAttributes(toAttributesProto(spanData.getAttributes(), resourceLabels)) + .setTimeEvents( + toTimeEventsProto(spanData.getAnnotations(), spanData.getMessageEvents())); + io.opencensus.trace.Status status = spanData.getStatus(); + if (status != null) { + spanBuilder.setStatus(toStatusProto(status)); + } + Timestamp end = spanData.getEndTimestamp(); + if (end != null) { + spanBuilder.setEndTime(toTimestampProto(end)); + } + spanBuilder.setLinks(toLinksProto(spanData.getLinks())); + Integer childSpanCount = spanData.getChildSpanCount(); + if (childSpanCount != null) { + spanBuilder.setChildSpanCount(Int32Value.newBuilder().setValue(childSpanCount).build()); + } + if (spanData.getParentSpanId() != null && spanData.getParentSpanId().isValid()) { + spanBuilder.setParentSpanId(spanData.getParentSpanId().toLowerBase16()); + } + + return spanBuilder.build(); + } + + private static Span.TimeEvents toTimeEventsProto( + TimedEvents<Annotation> annotationTimedEvents, + TimedEvents<io.opencensus.trace.MessageEvent> messageEventTimedEvents) { + Span.TimeEvents.Builder timeEventsBuilder = Span.TimeEvents.newBuilder(); + timeEventsBuilder.setDroppedAnnotationsCount(annotationTimedEvents.getDroppedEventsCount()); + for (TimedEvent<Annotation> annotation : annotationTimedEvents.getEvents()) { + timeEventsBuilder.addTimeEvent(toTimeAnnotationProto(annotation)); + } + timeEventsBuilder.setDroppedMessageEventsCount(messageEventTimedEvents.getDroppedEventsCount()); + for (TimedEvent<io.opencensus.trace.MessageEvent> networkEvent : + messageEventTimedEvents.getEvents()) { + timeEventsBuilder.addTimeEvent(toTimeMessageEventProto(networkEvent)); + } + return timeEventsBuilder.build(); + } + + private static TimeEvent toTimeAnnotationProto(TimedEvent<Annotation> timedEvent) { + TimeEvent.Builder timeEventBuilder = + TimeEvent.newBuilder().setTime(toTimestampProto(timedEvent.getTimestamp())); + Annotation annotation = timedEvent.getEvent(); + timeEventBuilder.setAnnotation( + TimeEvent.Annotation.newBuilder() + .setDescription(toTruncatableStringProto(annotation.getDescription())) + .setAttributes(toAttributesBuilderProto(annotation.getAttributes(), 0)) + .build()); + return timeEventBuilder.build(); + } + + private static TimeEvent toTimeMessageEventProto( + TimedEvent<io.opencensus.trace.MessageEvent> timedEvent) { + TimeEvent.Builder timeEventBuilder = + TimeEvent.newBuilder().setTime(toTimestampProto(timedEvent.getTimestamp())); + io.opencensus.trace.MessageEvent messageEvent = timedEvent.getEvent(); + timeEventBuilder.setMessageEvent( + TimeEvent.MessageEvent.newBuilder() + .setId(messageEvent.getMessageId()) + .setCompressedSizeBytes(messageEvent.getCompressedMessageSize()) + .setUncompressedSizeBytes(messageEvent.getUncompressedMessageSize()) + .setType(toMessageEventTypeProto(messageEvent)) + .build()); + return timeEventBuilder.build(); + } + + private static TimeEvent.MessageEvent.Type toMessageEventTypeProto( + io.opencensus.trace.MessageEvent messageEvent) { + if (messageEvent.getType() == Type.RECEIVED) { + return MessageEvent.Type.RECEIVED; + } else { + return MessageEvent.Type.SENT; + } + } + + // These are the attributes of the Span, where usually we may add more attributes like the agent. + private static Attributes toAttributesProto( + io.opencensus.trace.export.SpanData.Attributes attributes, + Map<String, AttributeValue> resourceLabels) { + Attributes.Builder attributesBuilder = + toAttributesBuilderProto( + attributes.getAttributeMap(), attributes.getDroppedAttributesCount()); + attributesBuilder.putAttributeMap(AGENT_LABEL_KEY, AGENT_LABEL_VALUE); + for (Entry<String, AttributeValue> entry : resourceLabels.entrySet()) { + attributesBuilder.putAttributeMap(entry.getKey(), entry.getValue()); + } + return attributesBuilder.build(); + } + + private static Attributes.Builder toAttributesBuilderProto( + Map<String, io.opencensus.trace.AttributeValue> attributes, int droppedAttributesCount) { + Attributes.Builder attributesBuilder = + Attributes.newBuilder().setDroppedAttributesCount(droppedAttributesCount); + for (Map.Entry<String, io.opencensus.trace.AttributeValue> label : attributes.entrySet()) { + AttributeValue value = toAttributeValueProto(label.getValue()); + if (value != null) { + attributesBuilder.putAttributeMap(mapKey(label.getKey()), value); + } + } + return attributesBuilder; + } + + @VisibleForTesting + static Map<String, AttributeValue> getResourceLabels( + @javax.annotation.Nullable MonitoredResource resource) { + if (resource == null) { + return Collections.emptyMap(); + } + Map<String, AttributeValue> resourceLabels = new HashMap<String, AttributeValue>(); + ResourceType resourceType = resource.getResourceType(); + switch (resourceType) { + case AWS_EC2_INSTANCE: + AwsEc2InstanceMonitoredResource awsEc2InstanceMonitoredResource = + (AwsEc2InstanceMonitoredResource) resource; + putToResourceAttributeMap( + resourceLabels, + resourceType, + "aws_account", + awsEc2InstanceMonitoredResource.getAccount()); + putToResourceAttributeMap( + resourceLabels, + resourceType, + "instance_id", + awsEc2InstanceMonitoredResource.getInstanceId()); + putToResourceAttributeMap( + resourceLabels, + resourceType, + "region", + "aws:" + awsEc2InstanceMonitoredResource.getRegion()); + return Collections.unmodifiableMap(resourceLabels); + case GCP_GCE_INSTANCE: + GcpGceInstanceMonitoredResource gcpGceInstanceMonitoredResource = + (GcpGceInstanceMonitoredResource) resource; + putToResourceAttributeMap( + resourceLabels, + resourceType, + "project_id", + gcpGceInstanceMonitoredResource.getAccount()); + putToResourceAttributeMap( + resourceLabels, + resourceType, + "instance_id", + gcpGceInstanceMonitoredResource.getInstanceId()); + putToResourceAttributeMap( + resourceLabels, resourceType, "zone", gcpGceInstanceMonitoredResource.getZone()); + return Collections.unmodifiableMap(resourceLabels); + case GCP_GKE_CONTAINER: + GcpGkeContainerMonitoredResource gcpGkeContainerMonitoredResource = + (GcpGkeContainerMonitoredResource) resource; + putToResourceAttributeMap( + resourceLabels, + resourceType, + "project_id", + gcpGkeContainerMonitoredResource.getAccount()); + putToResourceAttributeMap( + resourceLabels, resourceType, "location", gcpGkeContainerMonitoredResource.getZone()); + putToResourceAttributeMap( + resourceLabels, + resourceType, + "cluster_name", + gcpGkeContainerMonitoredResource.getClusterName()); + putToResourceAttributeMap( + resourceLabels, + resourceType, + "container_name", + gcpGkeContainerMonitoredResource.getContainerName()); + putToResourceAttributeMap( + resourceLabels, + resourceType, + "namespace_name", + gcpGkeContainerMonitoredResource.getNamespaceId()); + putToResourceAttributeMap( + resourceLabels, resourceType, "pod_name", gcpGkeContainerMonitoredResource.getPodId()); + return Collections.unmodifiableMap(resourceLabels); + } + return Collections.emptyMap(); + } + + private static void putToResourceAttributeMap( + Map<String, AttributeValue> map, + ResourceType resourceType, + String attributeName, + String attributeValue) { + map.put( + createResourceLabelKey(resourceType, attributeName), + toStringAttributeValueProto(attributeValue)); + } + + @VisibleForTesting + static String createResourceLabelKey(ResourceType resourceType, String resourceAttribute) { + return String.format("g.co/r/%s/%s", mapToStringResourceType(resourceType), resourceAttribute); + } + + private static String mapToStringResourceType(ResourceType resourceType) { + switch (resourceType) { + case GCP_GCE_INSTANCE: + return "gce_instance"; + case GCP_GKE_CONTAINER: + return "k8s_container"; + case AWS_EC2_INSTANCE: + return "aws_ec2_instance"; + } + throw new IllegalArgumentException("Unknown resource type."); + } + + @VisibleForTesting + static AttributeValue toStringAttributeValueProto(String value) { + return AttributeValue.newBuilder().setStringValue(toTruncatableStringProto(value)).build(); + } + + private static String mapKey(String key) { + if (HTTP_ATTRIBUTE_MAPPING.containsKey(key)) { + return HTTP_ATTRIBUTE_MAPPING.get(key); + } else { + return key; + } + } + + private static Status toStatusProto(io.opencensus.trace.Status status) { + Status.Builder statusBuilder = Status.newBuilder().setCode(status.getCanonicalCode().value()); + if (status.getDescription() != null) { + statusBuilder.setMessage(status.getDescription()); + } + return statusBuilder.build(); + } + + private static TruncatableString toTruncatableStringProto(String string) { + return TruncatableString.newBuilder().setValue(string).setTruncatedByteCount(0).build(); + } + + private static com.google.protobuf.Timestamp toTimestampProto(Timestamp timestamp) { + return com.google.protobuf.Timestamp.newBuilder() + .setSeconds(timestamp.getSeconds()) + .setNanos(timestamp.getNanos()) + .build(); + } + + @javax.annotation.Nullable + private static AttributeValue toAttributeValueProto( + io.opencensus.trace.AttributeValue attributeValue) { + return attributeValue.match( + stringAttributeValueFunction, + booleanAttributeValueFunction, + longAttributeValueFunction, + doubleAttributeValueFunction, + Functions.</*@Nullable*/ AttributeValue>returnNull()); + } + + private static Link.Type toLinkTypeProto(io.opencensus.trace.Link.Type type) { + if (type == io.opencensus.trace.Link.Type.PARENT_LINKED_SPAN) { + return Link.Type.PARENT_LINKED_SPAN; + } else { + return Link.Type.CHILD_LINKED_SPAN; + } + } + + private static String toDisplayName(String spanName, @javax.annotation.Nullable Kind spanKind) { + if (spanKind == Kind.SERVER && !spanName.startsWith(SERVER_PREFIX)) { + return SERVER_PREFIX + spanName; + } + + if (spanKind == Kind.CLIENT && !spanName.startsWith(CLIENT_PREFIX)) { + return CLIENT_PREFIX + spanName; + } + + return spanName; + } + + private static Link toLinkProto(io.opencensus.trace.Link link) { + checkNotNull(link); + return Link.newBuilder() + .setTraceId(link.getTraceId().toLowerBase16()) + .setSpanId(link.getSpanId().toLowerBase16()) + .setType(toLinkTypeProto(link.getType())) + .setAttributes(toAttributesBuilderProto(link.getAttributes(), 0)) + .build(); + } + + private static Links toLinksProto(io.opencensus.trace.export.SpanData.Links links) { + final Links.Builder linksBuilder = + Links.newBuilder().setDroppedLinksCount(links.getDroppedLinksCount()); + for (io.opencensus.trace.Link link : links.getLinks()) { + linksBuilder.addLink(toLinkProto(link)); + } + return linksBuilder.build(); + } + + @Override + public void export(Collection<SpanData> spanDataList) { + // Start a new span with explicit 1/10000 sampling probability to avoid the case when user + // sets the default sampler to always sample and we get the gRPC span of the stackdriver + // export call always sampled and go to an infinite loop. + Scope scope = + tracer + .spanBuilder("ExportStackdriverTraces") + .setSampler(probabilitySampler) + .setRecordEvents(true) + .startScopedSpan(); + try { + List<Span> spans = new ArrayList<>(spanDataList.size()); + for (SpanData spanData : spanDataList) { + spans.add(generateSpan(spanData, RESOURCE_LABELS)); + } + // Sync call because it is already called for a batch of data, and on a separate thread. + // TODO(bdrutu): Consider to make this async in the future. + traceServiceClient.batchWriteSpans(projectName, spans); + } finally { + scope.close(); + } + } +} diff --git a/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfigurationTest.java b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfigurationTest.java new file mode 100644 index 00000000..6926e869 --- /dev/null +++ b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfigurationTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2018, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.exporter.trace.stackdriver; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.auth.Credentials; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; +import java.util.Date; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link StackdriverTraceConfiguration}. */ +@RunWith(JUnit4.class) +public class StackdriverTraceConfigurationTest { + + private static final Credentials FAKE_CREDENTIALS = + GoogleCredentials.newBuilder().setAccessToken(new AccessToken("fake", new Date(100))).build(); + private static final String PROJECT_ID = "project"; + + @Test + public void defaultConfiguration() { + StackdriverTraceConfiguration configuration = StackdriverTraceConfiguration.builder().build(); + assertThat(configuration.getCredentials()).isNull(); + assertThat(configuration.getProjectId()).isNull(); + } + + @Test + public void updateAll() { + StackdriverTraceConfiguration configuration = + StackdriverTraceConfiguration.builder() + .setCredentials(FAKE_CREDENTIALS) + .setProjectId(PROJECT_ID) + .build(); + assertThat(configuration.getCredentials()).isEqualTo(FAKE_CREDENTIALS); + assertThat(configuration.getProjectId()).isEqualTo(PROJECT_ID); + } +} diff --git a/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporterTest.java b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporterTest.java new file mode 100644 index 00000000..6a12a899 --- /dev/null +++ b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporterTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.exporter.trace.stackdriver; + +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.same; +import static org.mockito.Mockito.verify; + +import io.opencensus.trace.export.SpanExporter; +import io.opencensus.trace.export.SpanExporter.Handler; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Unit tests for {@link StackdriverTraceExporter}. */ +@RunWith(JUnit4.class) +public class StackdriverTraceExporterTest { + @Mock private SpanExporter spanExporter; + @Mock private Handler handler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void registerUnregisterStackdriverExporter() { + StackdriverTraceExporter.register(spanExporter, handler); + verify(spanExporter) + .registerHandler( + eq("io.opencensus.exporter.trace.stackdriver.StackdriverTraceExporter"), same(handler)); + StackdriverTraceExporter.unregister(spanExporter); + verify(spanExporter) + .unregisterHandler(eq("io.opencensus.exporter.trace.stackdriver.StackdriverTraceExporter")); + } +} diff --git a/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerExportTest.java b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerExportTest.java new file mode 100644 index 00000000..32458597 --- /dev/null +++ b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerExportTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2018, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.exporter.trace.stackdriver; + +import static org.mockito.Mockito.when; + +import com.google.cloud.trace.v2.TraceServiceClient; +import com.google.cloud.trace.v2.stub.TraceServiceStub; +import io.opencensus.trace.export.SpanData; +import java.util.Collection; +import java.util.Collections; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Unit tests for exporting in {@link StackdriverV2ExporterHandler}. */ +@RunWith(JUnit4.class) +public final class StackdriverV2ExporterHandlerExportTest { + private static final String PROJECT_ID = "PROJECT_ID"; + // mock the service stub to provide a fake trace service. + @Mock private TraceServiceStub traceServiceStub; + private TraceServiceClient traceServiceClient; + @Rule public final ExpectedException thrown = ExpectedException.none(); + + private StackdriverV2ExporterHandler handler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + // TODO(@Hailong): TraceServiceClient.create(TraceServiceStub) is a beta API and might change + // in the future. + traceServiceClient = TraceServiceClient.create(traceServiceStub); + handler = new StackdriverV2ExporterHandler(PROJECT_ID, traceServiceClient); + } + + @Test + public void export() { + when(traceServiceStub.batchWriteSpansCallable()) + .thenThrow(new RuntimeException("TraceServiceStub called")); + Collection<SpanData> spanDataList = Collections.<SpanData>emptyList(); + thrown.expect(RuntimeException.class); + thrown.expectMessage("TraceServiceStub called"); + handler.export(spanDataList); + } +} diff --git a/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerProtoTest.java b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerProtoTest.java new file mode 100644 index 00000000..8b28dc06 --- /dev/null +++ b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerProtoTest.java @@ -0,0 +1,489 @@ +/* + * Copyright 2018, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opencensus.exporter.trace.stackdriver; + +import static com.google.common.truth.Truth.assertThat; +import static io.opencensus.contrib.monitoredresource.util.ResourceType.AWS_EC2_INSTANCE; +import static io.opencensus.contrib.monitoredresource.util.ResourceType.GCP_GCE_INSTANCE; +import static io.opencensus.contrib.monitoredresource.util.ResourceType.GCP_GKE_CONTAINER; +import static io.opencensus.exporter.trace.stackdriver.StackdriverV2ExporterHandler.createResourceLabelKey; +import static io.opencensus.exporter.trace.stackdriver.StackdriverV2ExporterHandler.toStringAttributeValueProto; + +import com.google.auth.Credentials; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.devtools.cloudtrace.v2.AttributeValue; +import com.google.devtools.cloudtrace.v2.Span; +import com.google.devtools.cloudtrace.v2.Span.TimeEvent; +import com.google.devtools.cloudtrace.v2.Span.TimeEvent.MessageEvent; +import com.google.devtools.cloudtrace.v2.StackTrace; +import com.google.devtools.cloudtrace.v2.TruncatableString; +import com.google.protobuf.Int32Value; +import io.opencensus.common.Timestamp; +import io.opencensus.contrib.monitoredresource.util.MonitoredResource; +import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource; +import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource; +import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource; +import io.opencensus.trace.Annotation; +import io.opencensus.trace.Link; +import io.opencensus.trace.Span.Kind; +import io.opencensus.trace.SpanContext; +import io.opencensus.trace.SpanId; +import io.opencensus.trace.Status; +import io.opencensus.trace.TraceId; +import io.opencensus.trace.TraceOptions; +import io.opencensus.trace.export.SpanData; +import io.opencensus.trace.export.SpanData.TimedEvent; +import io.opencensus.trace.export.SpanData.TimedEvents; +import java.io.IOException; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for proto conversions in {@link StackdriverV2ExporterHandler}. */ +@RunWith(JUnit4.class) +public final class StackdriverV2ExporterHandlerProtoTest { + + private static final Credentials FAKE_CREDENTIALS = + GoogleCredentials.newBuilder().setAccessToken(new AccessToken("fake", new Date(100))).build(); + // OpenCensus constants + private static final Timestamp startTimestamp = Timestamp.create(123, 456); + private static final Timestamp eventTimestamp1 = Timestamp.create(123, 457); + private static final Timestamp eventTimestamp2 = Timestamp.create(123, 458); + private static final Timestamp eventTimestamp3 = Timestamp.create(123, 459); + private static final Timestamp endTimestamp = Timestamp.create(123, 460); + + private static final String PROJECT_ID = "PROJECT_ID"; + private static final String TRACE_ID = "4bf92f3577b34da6a3ce929d0e0e4736"; + private static final String SPAN_ID = "24aa0b2d371f48c9"; + private static final String PARENT_SPAN_ID = "71da8d631536f5f1"; + private static final String SPAN_NAME = "MySpanName"; + private static final String SD_SPAN_NAME = + String.format("projects/%s/traces/%s/spans/%s", PROJECT_ID, TRACE_ID, SPAN_ID); + private static final String ANNOTATION_TEXT = "MyAnnotationText"; + private static final String ATTRIBUTE_KEY_1 = "MyAttributeKey1"; + private static final String ATTRIBUTE_KEY_2 = "MyAttributeKey2"; + + private static final int DROPPED_ATTRIBUTES_COUNT = 1; + private static final int DROPPED_ANNOTATIONS_COUNT = 2; + private static final int DROPPED_NETWORKEVENTS_COUNT = 3; + private static final int DROPPED_LINKS_COUNT = 4; + private static final int CHILD_SPAN_COUNT = 13; + + private static final Annotation annotation = Annotation.fromDescription(ANNOTATION_TEXT); + private static final io.opencensus.trace.MessageEvent recvMessageEvent = + io.opencensus.trace.MessageEvent.builder(io.opencensus.trace.MessageEvent.Type.RECEIVED, 1) + .build(); + private static final io.opencensus.trace.MessageEvent sentMessageEvent = + io.opencensus.trace.MessageEvent.builder(io.opencensus.trace.MessageEvent.Type.SENT, 1) + .build(); + private static final Status status = Status.DEADLINE_EXCEEDED.withDescription("TooSlow"); + private static final SpanId parentSpanId = SpanId.fromLowerBase16(PARENT_SPAN_ID); + private static final SpanId spanId = SpanId.fromLowerBase16(SPAN_ID); + private static final TraceId traceId = TraceId.fromLowerBase16(TRACE_ID); + private static final TraceOptions traceOptions = TraceOptions.DEFAULT; + private static final SpanContext spanContext = SpanContext.create(traceId, spanId, traceOptions); + + private static final List<TimedEvent<Annotation>> annotationsList = + ImmutableList.of( + SpanData.TimedEvent.create(eventTimestamp1, annotation), + SpanData.TimedEvent.create(eventTimestamp3, annotation)); + private static final List<TimedEvent<io.opencensus.trace.MessageEvent>> networkEventsList = + ImmutableList.of( + SpanData.TimedEvent.create(eventTimestamp1, recvMessageEvent), + SpanData.TimedEvent.create(eventTimestamp2, sentMessageEvent)); + private static final List<Link> linksList = + ImmutableList.of(Link.fromSpanContext(spanContext, Link.Type.CHILD_LINKED_SPAN)); + + private static final SpanData.Attributes attributes = + SpanData.Attributes.create( + ImmutableMap.of( + ATTRIBUTE_KEY_1, + io.opencensus.trace.AttributeValue.longAttributeValue(10L), + ATTRIBUTE_KEY_2, + io.opencensus.trace.AttributeValue.booleanAttributeValue(true)), + DROPPED_ATTRIBUTES_COUNT); + private static final TimedEvents<Annotation> annotations = + TimedEvents.create(annotationsList, DROPPED_ANNOTATIONS_COUNT); + private static final TimedEvents<io.opencensus.trace.MessageEvent> messageEvents = + TimedEvents.create(networkEventsList, DROPPED_NETWORKEVENTS_COUNT); + private static final SpanData.Links links = SpanData.Links.create(linksList, DROPPED_LINKS_COUNT); + private static final Map<String, AttributeValue> EMPTY_RESOURCE_LABELS = Collections.emptyMap(); + private static final AwsEc2InstanceMonitoredResource AWS_EC2_INSTANCE_MONITORED_RESOURCE = + AwsEc2InstanceMonitoredResource.create("my-project", "my-instance", "us-east-1"); + private static final GcpGceInstanceMonitoredResource GCP_GCE_INSTANCE_MONITORED_RESOURCE = + GcpGceInstanceMonitoredResource.create("my-project", "my-instance", "us-east1"); + private static final GcpGkeContainerMonitoredResource GCP_GKE_CONTAINER_MONITORED_RESOURCE = + GcpGkeContainerMonitoredResource.create( + "my-project", "cluster", "container", "namespace", "my-instance", "pod", "us-east1"); + private static final ImmutableMap<String, AttributeValue> AWS_RESOURCE_LABELS = + ImmutableMap.of( + createResourceLabelKey(AWS_EC2_INSTANCE, "aws_account"), + toStringAttributeValueProto("my-project"), + createResourceLabelKey(AWS_EC2_INSTANCE, "instance_id"), + toStringAttributeValueProto("my-instance"), + createResourceLabelKey(AWS_EC2_INSTANCE, "region"), + toStringAttributeValueProto("aws:us-east-1")); + private static final ImmutableMap<String, AttributeValue> GCE_RESOURCE_LABELS = + ImmutableMap.of( + createResourceLabelKey(GCP_GCE_INSTANCE, "project_id"), + toStringAttributeValueProto("my-project"), + createResourceLabelKey(GCP_GCE_INSTANCE, "instance_id"), + toStringAttributeValueProto("my-instance"), + createResourceLabelKey(GCP_GCE_INSTANCE, "zone"), + toStringAttributeValueProto("us-east1")); + private static final ImmutableMap<String, AttributeValue> GKE_RESOURCE_LABELS = + ImmutableMap.<String, AttributeValue>builder() + .put( + createResourceLabelKey(GCP_GKE_CONTAINER, "project_id"), + toStringAttributeValueProto("my-project")) + .put( + createResourceLabelKey(GCP_GKE_CONTAINER, "cluster_name"), + toStringAttributeValueProto("cluster")) + .put( + createResourceLabelKey(GCP_GKE_CONTAINER, "container_name"), + toStringAttributeValueProto("container")) + .put( + createResourceLabelKey(GCP_GKE_CONTAINER, "namespace_name"), + toStringAttributeValueProto("namespace")) + .put( + createResourceLabelKey(GCP_GKE_CONTAINER, "pod_name"), + toStringAttributeValueProto("pod")) + .put( + createResourceLabelKey(GCP_GKE_CONTAINER, "location"), + toStringAttributeValueProto("us-east1")) + .build(); + + private StackdriverV2ExporterHandler handler; + + @Before + public void setUp() throws IOException { + handler = StackdriverV2ExporterHandler.createWithCredentials(FAKE_CREDENTIALS, PROJECT_ID); + } + + @Test + public void generateSpan() { + SpanData spanData = + SpanData.create( + spanContext, + parentSpanId, + /* hasRemoteParent= */ true, + SPAN_NAME, + null, + startTimestamp, + attributes, + annotations, + messageEvents, + links, + CHILD_SPAN_COUNT, + status, + endTimestamp); + TimeEvent annotationTimeEvent1 = + TimeEvent.newBuilder() + .setAnnotation( + TimeEvent.Annotation.newBuilder() + .setDescription( + TruncatableString.newBuilder().setValue(ANNOTATION_TEXT).build()) + .setAttributes(Span.Attributes.newBuilder().build()) + .build()) + .setTime( + com.google.protobuf.Timestamp.newBuilder() + .setSeconds(eventTimestamp1.getSeconds()) + .setNanos(eventTimestamp1.getNanos()) + .build()) + .build(); + TimeEvent annotationTimeEvent2 = + TimeEvent.newBuilder() + .setAnnotation( + TimeEvent.Annotation.newBuilder() + .setDescription( + TruncatableString.newBuilder().setValue(ANNOTATION_TEXT).build()) + .setAttributes(Span.Attributes.newBuilder().build()) + .build()) + .setTime( + com.google.protobuf.Timestamp.newBuilder() + .setSeconds(eventTimestamp3.getSeconds()) + .setNanos(eventTimestamp3.getNanos()) + .build()) + .build(); + + TimeEvent sentTimeEvent = + TimeEvent.newBuilder() + .setMessageEvent( + TimeEvent.MessageEvent.newBuilder() + .setType(MessageEvent.Type.SENT) + .setId(sentMessageEvent.getMessageId())) + .setTime( + com.google.protobuf.Timestamp.newBuilder() + .setSeconds(eventTimestamp2.getSeconds()) + .setNanos(eventTimestamp2.getNanos()) + .build()) + .build(); + TimeEvent recvTimeEvent = + TimeEvent.newBuilder() + .setMessageEvent( + TimeEvent.MessageEvent.newBuilder() + .setType(MessageEvent.Type.RECEIVED) + .setId(recvMessageEvent.getMessageId())) + .setTime( + com.google.protobuf.Timestamp.newBuilder() + .setSeconds(eventTimestamp1.getSeconds()) + .setNanos(eventTimestamp1.getNanos()) + .build()) + .build(); + + Span.Links spanLinks = + Span.Links.newBuilder() + .setDroppedLinksCount(DROPPED_LINKS_COUNT) + .addLink( + Span.Link.newBuilder() + .setType(Span.Link.Type.CHILD_LINKED_SPAN) + .setTraceId(TRACE_ID) + .setSpanId(SPAN_ID) + .setAttributes(Span.Attributes.newBuilder().build()) + .build()) + .build(); + + com.google.rpc.Status spanStatus = + com.google.rpc.Status.newBuilder() + .setCode(com.google.rpc.Code.DEADLINE_EXCEEDED.getNumber()) + .setMessage("TooSlow") + .build(); + + com.google.protobuf.Timestamp startTime = + com.google.protobuf.Timestamp.newBuilder() + .setSeconds(startTimestamp.getSeconds()) + .setNanos(startTimestamp.getNanos()) + .build(); + com.google.protobuf.Timestamp endTime = + com.google.protobuf.Timestamp.newBuilder() + .setSeconds(endTimestamp.getSeconds()) + .setNanos(endTimestamp.getNanos()) + .build(); + + Span span = handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS); + assertThat(span.getName()).isEqualTo(SD_SPAN_NAME); + assertThat(span.getSpanId()).isEqualTo(SPAN_ID); + assertThat(span.getParentSpanId()).isEqualTo(PARENT_SPAN_ID); + assertThat(span.getDisplayName()) + .isEqualTo(TruncatableString.newBuilder().setValue(SPAN_NAME).build()); + assertThat(span.getStartTime()).isEqualTo(startTime); + assertThat(span.getEndTime()).isEqualTo(endTime); + assertThat(span.getAttributes().getDroppedAttributesCount()) + .isEqualTo(DROPPED_ATTRIBUTES_COUNT); + // The generated attributes map contains more values (e.g. agent). We only test what we added. + assertThat(span.getAttributes().getAttributeMapMap()) + .containsEntry(ATTRIBUTE_KEY_1, AttributeValue.newBuilder().setIntValue(10L).build()); + assertThat(span.getAttributes().getAttributeMapMap()) + .containsEntry(ATTRIBUTE_KEY_2, AttributeValue.newBuilder().setBoolValue(true).build()); + // TODO(@Hailong): add stack trace test in the future. + assertThat(span.getStackTrace()).isEqualTo(StackTrace.newBuilder().build()); + assertThat(span.getTimeEvents().getDroppedMessageEventsCount()) + .isEqualTo(DROPPED_NETWORKEVENTS_COUNT); + assertThat(span.getTimeEvents().getDroppedAnnotationsCount()) + .isEqualTo(DROPPED_ANNOTATIONS_COUNT); + assertThat(span.getTimeEvents().getTimeEventList()) + .containsAllOf(annotationTimeEvent1, annotationTimeEvent2, sentTimeEvent, recvTimeEvent); + assertThat(span.getLinks()).isEqualTo(spanLinks); + assertThat(span.getStatus()).isEqualTo(spanStatus); + assertThat(span.getSameProcessAsParentSpan()) + .isEqualTo(com.google.protobuf.BoolValue.newBuilder().build()); + assertThat(span.getChildSpanCount()) + .isEqualTo(Int32Value.newBuilder().setValue(CHILD_SPAN_COUNT).build()); + } + + @Test + public void getResourceLabels_AwsEc2ResourceLabels() { + testGetResourceLabels(AWS_EC2_INSTANCE_MONITORED_RESOURCE, AWS_RESOURCE_LABELS); + } + + @Test + public void getResourceLabels_GceResourceLabels() { + testGetResourceLabels(GCP_GCE_INSTANCE_MONITORED_RESOURCE, GCE_RESOURCE_LABELS); + } + + @Test + public void getResourceLabels_GkeResourceLabels() { + testGetResourceLabels(GCP_GKE_CONTAINER_MONITORED_RESOURCE, GKE_RESOURCE_LABELS); + } + + private static void testGetResourceLabels( + MonitoredResource resource, Map<String, AttributeValue> expectedLabels) { + Map<String, AttributeValue> actualLabels = + StackdriverV2ExporterHandler.getResourceLabels(resource); + assertThat(actualLabels).containsExactlyEntriesIn(expectedLabels); + } + + @Test + public void generateSpan_WithResourceLabels() { + SpanData spanData = + SpanData.create( + spanContext, + parentSpanId, + /* hasRemoteParent= */ true, + SPAN_NAME, + null, + startTimestamp, + attributes, + annotations, + messageEvents, + links, + CHILD_SPAN_COUNT, + status, + endTimestamp); + Span span = handler.generateSpan(spanData, AWS_RESOURCE_LABELS); + Map<String, AttributeValue> attributeMap = span.getAttributes().getAttributeMapMap(); + assertThat(attributeMap.entrySet()).containsAllIn(AWS_RESOURCE_LABELS.entrySet()); + } + + @Test + public void mapHttpAttributes() { + Map<String, io.opencensus.trace.AttributeValue> attributesMap = + new HashMap<String, io.opencensus.trace.AttributeValue>(); + + attributesMap.put("http.host", io.opencensus.trace.AttributeValue.stringAttributeValue("host")); + attributesMap.put( + "http.method", io.opencensus.trace.AttributeValue.stringAttributeValue("method")); + attributesMap.put("http.path", io.opencensus.trace.AttributeValue.stringAttributeValue("path")); + attributesMap.put( + "http.route", io.opencensus.trace.AttributeValue.stringAttributeValue("route")); + attributesMap.put( + "http.user_agent", io.opencensus.trace.AttributeValue.stringAttributeValue("user_agent")); + attributesMap.put( + "http.status_code", io.opencensus.trace.AttributeValue.longAttributeValue(200L)); + SpanData.Attributes httpAttributes = SpanData.Attributes.create(attributesMap, 0); + + SpanData spanData = + SpanData.create( + spanContext, + parentSpanId, + /* hasRemoteParent= */ true, + SPAN_NAME, + startTimestamp, + httpAttributes, + annotations, + messageEvents, + links, + CHILD_SPAN_COUNT, + status, + endTimestamp); + + Span span = handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS); + Map<String, AttributeValue> attributes = span.getAttributes().getAttributeMapMap(); + + assertThat(attributes).containsEntry("/http/host", toStringAttributeValueProto("host")); + assertThat(attributes).containsEntry("/http/method", toStringAttributeValueProto("method")); + assertThat(attributes).containsEntry("/http/path", toStringAttributeValueProto("path")); + assertThat(attributes).containsEntry("/http/route", toStringAttributeValueProto("route")); + assertThat(attributes) + .containsEntry("/http/user_agent", toStringAttributeValueProto("user_agent")); + assertThat(attributes) + .containsEntry("/http/status_code", AttributeValue.newBuilder().setIntValue(200L).build()); + } + + @Test + public void generateSpanName_ForServer() { + SpanData spanData = + SpanData.create( + spanContext, + parentSpanId, + /* hasRemoteParent= */ true, + SPAN_NAME, + Kind.SERVER, + startTimestamp, + attributes, + annotations, + messageEvents, + links, + CHILD_SPAN_COUNT, + status, + endTimestamp); + assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue()) + .isEqualTo("Recv." + SPAN_NAME); + } + + @Test + public void generateSpanName_ForServerWithRecv() { + SpanData spanData = + SpanData.create( + spanContext, + parentSpanId, + /* hasRemoteParent= */ true, + "Recv." + SPAN_NAME, + Kind.SERVER, + startTimestamp, + attributes, + annotations, + messageEvents, + links, + CHILD_SPAN_COUNT, + status, + endTimestamp); + assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue()) + .isEqualTo("Recv." + SPAN_NAME); + } + + @Test + public void generateSpanName_ForClient() { + SpanData spanData = + SpanData.create( + spanContext, + parentSpanId, + /* hasRemoteParent= */ true, + SPAN_NAME, + Kind.CLIENT, + startTimestamp, + attributes, + annotations, + messageEvents, + links, + CHILD_SPAN_COUNT, + status, + endTimestamp); + assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue()) + .isEqualTo("Sent." + SPAN_NAME); + } + + @Test + public void generateSpanName_ForClientWithSent() { + SpanData spanData = + SpanData.create( + spanContext, + parentSpanId, + /* hasRemoteParent= */ true, + "Sent." + SPAN_NAME, + Kind.CLIENT, + startTimestamp, + attributes, + annotations, + messageEvents, + links, + CHILD_SPAN_COUNT, + status, + endTimestamp); + assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue()) + .isEqualTo("Sent." + SPAN_NAME); + } +} |