diff --git a/buildscripts/import-control.xml b/buildscripts/import-control.xml index 4fb0eac8a9..59c3e20558 100644 --- a/buildscripts/import-control.xml +++ b/buildscripts/import-control.xml @@ -141,6 +141,7 @@ General guidelines on imports: + diff --git a/exporters/trace/stackdriver/build.gradle b/exporters/trace/stackdriver/build.gradle index fa4865616c..fc8bfef4b2 100644 --- a/exporters/trace/stackdriver/build.gradle +++ b/exporters/trace/stackdriver/build.gradle @@ -9,6 +9,7 @@ dependencies { compileOnly libraries.auto_value compile project(':opencensus-api'), + project(':opencensus-contrib-monitored-resource-util'), libraries.google_auth compile (libraries.google_cloud_trace) { 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 index 3c3f783c07..0993fd8005 100644 --- 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 @@ -42,6 +42,12 @@ 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; @@ -60,8 +66,10 @@ 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; @@ -91,6 +99,12 @@ final class StackdriverV2ExporterHandler extends SpanExporter.Handler { .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 RESOURCE_LABELS = getResourceLabels(RESOURCE); + private final String projectId; private final TraceServiceClient traceServiceClient; private final ProjectName projectName; @@ -122,7 +136,7 @@ static StackdriverV2ExporterHandler create(String projectId) throws IOException } @VisibleForTesting - Span generateSpan(SpanData spanData) { + Span generateSpan(SpanData spanData, Map resourceLabels) { SpanContext context = spanData.getContext(); final String traceIdHex = encodeTraceId(context.getTraceId()); final String spanIdHex = encodeSpanId(context.getSpanId()); @@ -135,7 +149,7 @@ Span generateSpan(SpanData spanData) { .setDisplayName( toTruncatableStringProto(toDisplayName(spanData.getName(), spanData.getKind()))) .setStartTime(toTimestampProto(spanData.getStartTimestamp())) - .setAttributes(toAttributesProto(spanData.getAttributes())) + .setAttributes(toAttributesProto(spanData.getAttributes(), resourceLabels)) .setTimeEvents( toTimeEventsProto(spanData.getAnnotations(), spanData.getMessageEvents())); io.opencensus.trace.Status status = spanData.getStatus(); @@ -220,11 +234,15 @@ private static TimeEvent.MessageEvent.Type toMessageEventTypeProto( // 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) { + io.opencensus.trace.export.SpanData.Attributes attributes, + Map resourceLabels) { Attributes.Builder attributesBuilder = toAttributesBuilderProto( attributes.getAttributeMap(), attributes.getDroppedAttributesCount()); attributesBuilder.putAttributeMap(AGENT_LABEL_KEY, AGENT_LABEL_VALUE); + for (Entry entry : resourceLabels.entrySet()) { + attributesBuilder.putAttributeMap(entry.getKey(), entry.getValue()); + } return attributesBuilder.build(); } @@ -239,6 +257,119 @@ private static Attributes.Builder toAttributesBuilderProto( return attributesBuilder; } + // TODO(songya): make constructor of MonitoredResource public, and add unit tests for this method. + private static Map getResourceLabels( + @javax.annotation.Nullable MonitoredResource resource) { + if (resource == null) { + return Collections.emptyMap(); + } + Map resourceLabels = new HashMap(); + 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, + "instance_id", + gcpGkeContainerMonitoredResource.getInstanceId()); + putToResourceAttributeMap( + resourceLabels, resourceType, "zone", gcpGkeContainerMonitoredResource.getZone()); + putToResourceAttributeMap( + resourceLabels, + resourceType, + "cluster_name", + gcpGkeContainerMonitoredResource.getClusterName()); + putToResourceAttributeMap( + resourceLabels, + resourceType, + "container_name", + gcpGkeContainerMonitoredResource.getContainerName()); + putToResourceAttributeMap( + resourceLabels, + resourceType, + "namespace_id", + gcpGkeContainerMonitoredResource.getNamespaceId()); + putToResourceAttributeMap( + resourceLabels, resourceType, "pod_id", gcpGkeContainerMonitoredResource.getPodId()); + return Collections.unmodifiableMap(resourceLabels); + } + return Collections.emptyMap(); + } + + private static void putToResourceAttributeMap( + Map 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 "gke_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); @@ -347,7 +478,7 @@ public void export(Collection spanDataList) { .startScopedSpan()) { List spans = new ArrayList<>(spanDataList.size()); for (SpanData spanData : spanDataList) { - spans.add(generateSpan(spanData)); + 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. 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 index f20d1021af..6674dd9153 100644 --- 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 @@ -17,6 +17,11 @@ 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; @@ -43,6 +48,7 @@ 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; @@ -119,6 +125,47 @@ public final class StackdriverV2ExporterHandlerProtoTest { private static final TimedEvents messageEvents = TimedEvents.create(networkEventsList, DROPPED_NETWORKEVENTS_COUNT); private static final SpanData.Links links = SpanData.Links.create(linksList, DROPPED_LINKS_COUNT); + private static final Map EMPTY_RESOURCE_LABELS = Collections.emptyMap(); + private static final ImmutableMap 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("us-east-1")); + private static final ImmutableMap 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 GKE_RESOURCE_LABELS = + ImmutableMap.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_id"), + toStringAttributeValueProto("namespace")) + .put( + createResourceLabelKey(GCP_GKE_CONTAINER, "instance_id"), + toStringAttributeValueProto("my-instance")) + .put( + createResourceLabelKey(GCP_GKE_CONTAINER, "pod_id"), + toStringAttributeValueProto("pod")) + .put( + createResourceLabelKey(GCP_GKE_CONTAINER, "zone"), + toStringAttributeValueProto("us-east1")) + .build(); private StackdriverV2ExporterHandler handler; @@ -227,7 +274,7 @@ public void generateSpan() { .setNanos(endTimestamp.getNanos()) .build(); - Span span = handler.generateSpan(spanData); + 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); @@ -258,6 +305,42 @@ public void generateSpan() { .isEqualTo(Int32Value.newBuilder().setValue(CHILD_SPAN_COUNT).build()); } + @Test + public void generateSpan_WithAwsEc2ResourceLabels() { + generateSpan_WithResourceLabels(AWS_RESOURCE_LABELS); + } + + @Test + public void generateSpan_WithGceResourceLabels() { + generateSpan_WithResourceLabels(GCE_RESOURCE_LABELS); + } + + @Test + public void generateSpan_WithGkeResourceLabels() { + generateSpan_WithResourceLabels(GKE_RESOURCE_LABELS); + } + + private void generateSpan_WithResourceLabels(Map resourceLabels) { + 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, resourceLabels); + Map attributeMap = span.getAttributes().getAttributeMapMap(); + assertThat(attributeMap.entrySet()).containsAllIn(resourceLabels.entrySet()); + } + @Test public void mapHttpAttributes() { Map attributesMap = @@ -290,14 +373,15 @@ public void mapHttpAttributes() { status, endTimestamp); - Span span = handler.generateSpan(spanData); + Span span = handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS); Map attributes = span.getAttributes().getAttributeMapMap(); - assertThat(attributes).containsEntry("/http/host", toStringValue("host")); - assertThat(attributes).containsEntry("/http/method", toStringValue("method")); - assertThat(attributes).containsEntry("/http/path", toStringValue("path")); - assertThat(attributes).containsEntry("/http/route", toStringValue("route")); - assertThat(attributes).containsEntry("/http/user_agent", toStringValue("user_agent")); + 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()); } @@ -319,7 +403,7 @@ public void generateSpanName_ForServer() { CHILD_SPAN_COUNT, status, endTimestamp); - assertThat(handler.generateSpan(spanData).getDisplayName().getValue()) + assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue()) .isEqualTo("Recv." + SPAN_NAME); } @@ -340,7 +424,7 @@ public void generateSpanName_ForServerWithRecv() { CHILD_SPAN_COUNT, status, endTimestamp); - assertThat(handler.generateSpan(spanData).getDisplayName().getValue()) + assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue()) .isEqualTo("Recv." + SPAN_NAME); } @@ -361,7 +445,7 @@ public void generateSpanName_ForClient() { CHILD_SPAN_COUNT, status, endTimestamp); - assertThat(handler.generateSpan(spanData).getDisplayName().getValue()) + assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue()) .isEqualTo("Sent." + SPAN_NAME); } @@ -382,14 +466,7 @@ public void generateSpanName_ForClientWithSent() { CHILD_SPAN_COUNT, status, endTimestamp); - assertThat(handler.generateSpan(spanData).getDisplayName().getValue()) + assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue()) .isEqualTo("Sent." + SPAN_NAME); } - - private static AttributeValue toStringValue(String value) { - return AttributeValue.newBuilder() - .setStringValue( - TruncatableString.newBuilder().setValue(value).setTruncatedByteCount(0).build()) - .build(); - } }