blob: de022c3fb9272e4c662593a8d985700ef6ec0baf [file] [log] [blame]
/*
* 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();
}
}
}