Add new components to allow for generating metrics from 100% of spans without impacting sampling (#802)
This commit is contained in:
parent
d305bf68c9
commit
fbf8304688
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.contrib.awsxray;
|
||||
|
||||
import io.opentelemetry.api.common.Attributes;
|
||||
import io.opentelemetry.api.trace.SpanKind;
|
||||
import io.opentelemetry.api.trace.TraceState;
|
||||
import io.opentelemetry.context.Context;
|
||||
import io.opentelemetry.sdk.trace.data.LinkData;
|
||||
import io.opentelemetry.sdk.trace.samplers.Sampler;
|
||||
import io.opentelemetry.sdk.trace.samplers.SamplingDecision;
|
||||
import io.opentelemetry.sdk.trace.samplers.SamplingResult;
|
||||
import java.util.List;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
/**
|
||||
* This sampler will return the sampling result of the provided {@link #rootSampler}, unless the
|
||||
* sampling result contains the sampling decision {@link SamplingDecision#DROP}, in which case, a
|
||||
* new sampling result will be returned that is functionally equivalent to the original, except that
|
||||
* it contains the sampling decision {@link SamplingDecision#RECORD_ONLY}. This ensures that all
|
||||
* spans are recorded, with no change to sampling.
|
||||
*
|
||||
* <p>The intended use case of this sampler is to provide a means of sending all spans to a
|
||||
* processor without having an impact on the sampling rate. This may be desirable if a user wishes
|
||||
* to count or otherwise measure all spans produced in a service, without incurring the cost of 100%
|
||||
* sampling.
|
||||
*/
|
||||
@Immutable
|
||||
public final class AlwaysRecordSampler implements Sampler {
|
||||
|
||||
private final Sampler rootSampler;
|
||||
|
||||
public static AlwaysRecordSampler create(Sampler rootSampler) {
|
||||
return new AlwaysRecordSampler(rootSampler);
|
||||
}
|
||||
|
||||
private AlwaysRecordSampler(Sampler rootSampler) {
|
||||
this.rootSampler = rootSampler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SamplingResult shouldSample(
|
||||
Context parentContext,
|
||||
String traceId,
|
||||
String name,
|
||||
SpanKind spanKind,
|
||||
Attributes attributes,
|
||||
List<LinkData> parentLinks) {
|
||||
SamplingResult result =
|
||||
rootSampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks);
|
||||
if (result.getDecision() == SamplingDecision.DROP) {
|
||||
result = wrapResultWithRecordOnlyResult(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "AlwaysRecordSampler{" + rootSampler.getDescription() + "}";
|
||||
}
|
||||
|
||||
private static SamplingResult wrapResultWithRecordOnlyResult(SamplingResult result) {
|
||||
return new SamplingResult() {
|
||||
@Override
|
||||
public SamplingDecision getDecision() {
|
||||
return SamplingDecision.RECORD_ONLY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Attributes getAttributes() {
|
||||
return result.getAttributes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TraceState getUpdatedTraceState(TraceState parentTraceState) {
|
||||
return result.getUpdatedTraceState(parentTraceState);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -12,6 +12,10 @@ final class AwsAttributeKeys {
|
|||
|
||||
private AwsAttributeKeys() {}
|
||||
|
||||
static final AttributeKey<String> AWS_SPAN_KIND = AttributeKey.stringKey("aws.span.kind");
|
||||
|
||||
static final AttributeKey<String> AWS_LOCAL_SERVICE = AttributeKey.stringKey("aws.local.service");
|
||||
|
||||
static final AttributeKey<String> AWS_LOCAL_OPERATION =
|
||||
AttributeKey.stringKey("aws.local.operation");
|
||||
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.contrib.awsxray;
|
||||
|
||||
import static io.opentelemetry.contrib.awsxray.AwsAttributeKeys.AWS_LOCAL_OPERATION;
|
||||
import static io.opentelemetry.contrib.awsxray.AwsAttributeKeys.AWS_LOCAL_SERVICE;
|
||||
import static io.opentelemetry.contrib.awsxray.AwsAttributeKeys.AWS_REMOTE_OPERATION;
|
||||
import static io.opentelemetry.contrib.awsxray.AwsAttributeKeys.AWS_REMOTE_SERVICE;
|
||||
import static io.opentelemetry.contrib.awsxray.AwsAttributeKeys.AWS_SPAN_KIND;
|
||||
import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_OPERATION;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_SYSTEM;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.FAAS_INVOKED_NAME;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.FAAS_INVOKED_PROVIDER;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.GRAPHQL_OPERATION_TYPE;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_OPERATION;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_SYSTEM;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.PEER_SERVICE;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.RPC_METHOD;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.RPC_SERVICE;
|
||||
|
||||
import io.opentelemetry.api.common.AttributeKey;
|
||||
import io.opentelemetry.api.common.Attributes;
|
||||
import io.opentelemetry.api.common.AttributesBuilder;
|
||||
import io.opentelemetry.api.trace.SpanKind;
|
||||
import io.opentelemetry.sdk.resources.Resource;
|
||||
import io.opentelemetry.sdk.trace.data.SpanData;
|
||||
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
|
||||
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* AwsMetricAttributeGenerator generates very specific metric attributes based on low-cardinality
|
||||
* span and resource attributes. If such attributes are not present, we fallback to default values.
|
||||
*
|
||||
* <p>The goal of these particular metric attributes is to get metrics for incoming and outgoing
|
||||
* traffic for a service. Namely, {@link SpanKind#SERVER} and {@link SpanKind#CONSUMER} spans
|
||||
* represent "incoming" traffic, {@link SpanKind#CLIENT} and {@link SpanKind#PRODUCER} spans
|
||||
* represent "outgoing" traffic, and {@link SpanKind#INTERNAL} spans are ignored.
|
||||
*/
|
||||
final class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
|
||||
|
||||
private static final Logger logger =
|
||||
Logger.getLogger(AwsMetricAttributeGenerator.class.getName());
|
||||
|
||||
// Special SERVICE attribute value if GRAPHQL_OPERATION_TYPE attribute key is present.
|
||||
private static final String GRAPHQL = "graphql";
|
||||
|
||||
// Default attribute values if no valid span attribute value is identified
|
||||
private static final String UNKNOWN_SERVICE = "UnknownService";
|
||||
private static final String UNKNOWN_OPERATION = "UnknownOperation";
|
||||
private static final String UNKNOWN_REMOTE_SERVICE = "UnknownRemoteService";
|
||||
private static final String UNKNOWN_REMOTE_OPERATION = "UnknownRemoteOperation";
|
||||
|
||||
@Override
|
||||
public Attributes generateMetricAttributesFromSpan(SpanData span, Resource resource) {
|
||||
AttributesBuilder builder = Attributes.builder();
|
||||
switch (span.getKind()) {
|
||||
case CONSUMER:
|
||||
case SERVER:
|
||||
setService(resource, span, builder);
|
||||
setIngressOperation(span, builder);
|
||||
setSpanKind(span, builder);
|
||||
break;
|
||||
case PRODUCER:
|
||||
case CLIENT:
|
||||
setService(resource, span, builder);
|
||||
setEgressOperation(span, builder);
|
||||
setRemoteServiceAndOperation(span, builder);
|
||||
setSpanKind(span, builder);
|
||||
break;
|
||||
default:
|
||||
// Add no attributes, signalling no metrics should be emitted.
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/** Service is always derived from {@link ResourceAttributes#SERVICE_NAME} */
|
||||
private static void setService(Resource resource, SpanData span, AttributesBuilder builder) {
|
||||
String service = resource.getAttribute(SERVICE_NAME);
|
||||
if (service == null) {
|
||||
logUnknownAttribute(AWS_LOCAL_SERVICE, span);
|
||||
service = UNKNOWN_SERVICE;
|
||||
}
|
||||
builder.put(AWS_LOCAL_SERVICE, service);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingress operation (i.e. operation for Server and Consumer spans) is always derived from span
|
||||
* name.
|
||||
*/
|
||||
private static void setIngressOperation(SpanData span, AttributesBuilder builder) {
|
||||
String operation = span.getName();
|
||||
if (operation == null) {
|
||||
logUnknownAttribute(AWS_LOCAL_OPERATION, span);
|
||||
operation = UNKNOWN_OPERATION;
|
||||
}
|
||||
builder.put(AWS_LOCAL_OPERATION, operation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Egress operation (i.e. operation for Client and Producer spans) is always derived from a
|
||||
* special span attribute, {@link AwsAttributeKeys#AWS_LOCAL_OPERATION}. This attribute is
|
||||
* generated with a separate SpanProcessor, {@link AttributePropagatingSpanProcessor}
|
||||
*/
|
||||
private static void setEgressOperation(SpanData span, AttributesBuilder builder) {
|
||||
String operation = span.getAttributes().get(AWS_LOCAL_OPERATION);
|
||||
if (operation == null) {
|
||||
logUnknownAttribute(AWS_LOCAL_OPERATION, span);
|
||||
operation = UNKNOWN_OPERATION;
|
||||
}
|
||||
builder.put(AWS_LOCAL_OPERATION, operation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remote attributes (only for Client and Producer spans) are generated based on low-cardinality
|
||||
* span attributes, in priority order.
|
||||
*
|
||||
* <p>The first priority is the AWS Remote attributes, which are generated from manually
|
||||
* instrumented span attributes, and are clear indications of customer intent. If AWS Remote
|
||||
* attributes are not present, the next highest priority span attribute is Peer Service, which is
|
||||
* also a reliable indicator of customer intent. If this is set, it will override
|
||||
* AWS_REMOTE_SERVICE identified from any other span attribute, other than AWS Remote attributes.
|
||||
*
|
||||
* <p>After this, we look for the following low-cardinality span attributes that can be used to
|
||||
* determine the remote metric attributes:
|
||||
*
|
||||
* <ul>
|
||||
* <li>RPC
|
||||
* <li>DB
|
||||
* <li>FAAS
|
||||
* <li>Messaging
|
||||
* <li>GraphQL - Special case, if {@link SemanticAttributes#GRAPHQL_OPERATION_TYPE} is present,
|
||||
* we use it for RemoteOperation and set RemoteService to {@link #GRAPHQL}.
|
||||
* </ul>
|
||||
*
|
||||
* <p>In each case, these span attributes were selected from the OpenTelemetry trace semantic
|
||||
* convention specifications as they adhere to the three following criteria:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Attributes are meaningfully indicative of remote service/operation names.
|
||||
* <li>Attributes are defined in the specification to be low cardinality, usually with a low-
|
||||
* cardinality list of values.
|
||||
* <li>Attributes are confirmed to have low-cardinality values, based on code analysis.
|
||||
* </ul>
|
||||
*
|
||||
* TODO: This specific logic may change in future. Specifically, we are still deciding which HTTP
|
||||
* and RPC attributes to use here, but this is a sufficient starting point.
|
||||
*/
|
||||
private static void setRemoteServiceAndOperation(SpanData span, AttributesBuilder builder) {
|
||||
if (isKeyPresent(span, AWS_REMOTE_SERVICE) || isKeyPresent(span, AWS_REMOTE_OPERATION)) {
|
||||
setRemoteService(span, builder, AWS_REMOTE_SERVICE);
|
||||
setRemoteOperation(span, builder, AWS_REMOTE_OPERATION);
|
||||
} else if (isKeyPresent(span, RPC_SERVICE) || isKeyPresent(span, RPC_METHOD)) {
|
||||
setRemoteService(span, builder, RPC_SERVICE);
|
||||
setRemoteOperation(span, builder, RPC_METHOD);
|
||||
} else if (isKeyPresent(span, DB_SYSTEM) || isKeyPresent(span, DB_OPERATION)) {
|
||||
setRemoteService(span, builder, DB_SYSTEM);
|
||||
setRemoteOperation(span, builder, DB_OPERATION);
|
||||
} else if (isKeyPresent(span, FAAS_INVOKED_PROVIDER) || isKeyPresent(span, FAAS_INVOKED_NAME)) {
|
||||
setRemoteService(span, builder, FAAS_INVOKED_PROVIDER);
|
||||
setRemoteOperation(span, builder, FAAS_INVOKED_NAME);
|
||||
} else if (isKeyPresent(span, MESSAGING_SYSTEM) || isKeyPresent(span, MESSAGING_OPERATION)) {
|
||||
setRemoteService(span, builder, MESSAGING_SYSTEM);
|
||||
setRemoteOperation(span, builder, MESSAGING_OPERATION);
|
||||
} else if (isKeyPresent(span, GRAPHQL_OPERATION_TYPE)) {
|
||||
builder.put(AWS_REMOTE_SERVICE, GRAPHQL);
|
||||
setRemoteOperation(span, builder, GRAPHQL_OPERATION_TYPE);
|
||||
} else {
|
||||
logUnknownAttribute(AWS_REMOTE_SERVICE, span);
|
||||
builder.put(AWS_REMOTE_SERVICE, UNKNOWN_REMOTE_SERVICE);
|
||||
logUnknownAttribute(AWS_REMOTE_OPERATION, span);
|
||||
builder.put(AWS_REMOTE_OPERATION, UNKNOWN_REMOTE_OPERATION);
|
||||
}
|
||||
|
||||
// Peer service takes priority as RemoteService over everything but AWS Remote.
|
||||
if (isKeyPresent(span, PEER_SERVICE) && !isKeyPresent(span, AWS_REMOTE_SERVICE)) {
|
||||
setRemoteService(span, builder, PEER_SERVICE);
|
||||
}
|
||||
}
|
||||
|
||||
/** Span kind is needed for differentiating metrics in the EMF exporter */
|
||||
private static void setSpanKind(SpanData span, AttributesBuilder builder) {
|
||||
String spanKind = span.getKind().name();
|
||||
builder.put(AWS_SPAN_KIND, spanKind);
|
||||
}
|
||||
|
||||
private static boolean isKeyPresent(SpanData span, AttributeKey<String> key) {
|
||||
return span.getAttributes().get(key) != null;
|
||||
}
|
||||
|
||||
private static void setRemoteService(
|
||||
SpanData span, AttributesBuilder builder, AttributeKey<String> remoteServiceKey) {
|
||||
String remoteService = span.getAttributes().get(remoteServiceKey);
|
||||
if (remoteService == null) {
|
||||
logUnknownAttribute(AWS_REMOTE_SERVICE, span);
|
||||
remoteService = UNKNOWN_REMOTE_SERVICE;
|
||||
}
|
||||
builder.put(AWS_REMOTE_SERVICE, remoteService);
|
||||
}
|
||||
|
||||
private static void setRemoteOperation(
|
||||
SpanData span, AttributesBuilder builder, AttributeKey<String> remoteOperationKey) {
|
||||
String remoteOperation = span.getAttributes().get(remoteOperationKey);
|
||||
if (remoteOperation == null) {
|
||||
logUnknownAttribute(AWS_REMOTE_OPERATION, span);
|
||||
remoteOperation = UNKNOWN_REMOTE_OPERATION;
|
||||
}
|
||||
builder.put(AWS_REMOTE_OPERATION, remoteOperation);
|
||||
}
|
||||
|
||||
private static void logUnknownAttribute(AttributeKey<String> attributeKey, SpanData span) {
|
||||
String[] params = {
|
||||
attributeKey.getKey(), span.getKind().name(), span.getSpanContext().getSpanId()
|
||||
};
|
||||
logger.log(Level.FINEST, "No valid {0} value found for {1} span {2}", params);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.contrib.awsxray;
|
||||
|
||||
import io.opentelemetry.api.common.AttributeKey;
|
||||
import io.opentelemetry.api.common.Attributes;
|
||||
import io.opentelemetry.sdk.common.CompletableResultCode;
|
||||
import io.opentelemetry.sdk.resources.Resource;
|
||||
import io.opentelemetry.sdk.trace.data.DelegatingSpanData;
|
||||
import io.opentelemetry.sdk.trace.data.SpanData;
|
||||
import io.opentelemetry.sdk.trace.export.SpanExporter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
/**
|
||||
* This exporter will update a span with metric attributes before exporting. It depends on a {@link
|
||||
* SpanExporter} being provided on instantiation, which the AwsSpanMetricsExporter will delegate
|
||||
* export to. Also, a {@link MetricAttributeGenerator} must be provided, which will provide a means
|
||||
* to determine attributes which should be applied to the span. Finally, a {@link Resource} must be
|
||||
* provided, which is used to generate metric attributes.
|
||||
*
|
||||
* <p>This exporter should be coupled with the {@link AwsSpanMetricsProcessor} using the same {@link
|
||||
* MetricAttributeGenerator}. This will result in metrics and spans being produced with common
|
||||
* attributes.
|
||||
*/
|
||||
@Immutable
|
||||
public class AwsMetricAttributesSpanExporter implements SpanExporter {
|
||||
|
||||
private final SpanExporter delegate;
|
||||
private final MetricAttributeGenerator generator;
|
||||
private final Resource resource;
|
||||
|
||||
/** Use {@link AwsMetricAttributesSpanExporterBuilder} to construct this exporter. */
|
||||
static AwsMetricAttributesSpanExporter create(
|
||||
SpanExporter delegate, MetricAttributeGenerator generator, Resource resource) {
|
||||
return new AwsMetricAttributesSpanExporter(delegate, generator, resource);
|
||||
}
|
||||
|
||||
private AwsMetricAttributesSpanExporter(
|
||||
SpanExporter delegate, MetricAttributeGenerator generator, Resource resource) {
|
||||
this.delegate = delegate;
|
||||
this.generator = generator;
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableResultCode export(Collection<SpanData> spans) {
|
||||
List<SpanData> modifiedSpans = addMetricAttributes(spans);
|
||||
return delegate.export(modifiedSpans);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableResultCode flush() {
|
||||
return delegate.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableResultCode shutdown() {
|
||||
return delegate.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
delegate.close();
|
||||
}
|
||||
|
||||
private List<SpanData> addMetricAttributes(Collection<SpanData> spans) {
|
||||
List<SpanData> modifiedSpans = new ArrayList<>();
|
||||
|
||||
for (SpanData span : spans) {
|
||||
Attributes attributes = generator.generateMetricAttributesFromSpan(span, resource);
|
||||
if (!attributes.isEmpty()) {
|
||||
span = wrapSpanWithAttributes(span, attributes);
|
||||
}
|
||||
modifiedSpans.add(span);
|
||||
}
|
||||
|
||||
return modifiedSpans;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link #export} works with a {@link SpanData}, which does not permit modification. However, we
|
||||
* need to add derived metric attributes to the span. To work around this, we will wrap the
|
||||
* SpanData with a {@link DelegatingSpanData} that simply passes through all API calls, except for
|
||||
* those pertaining to Attributes, i.e. {@link SpanData#getAttributes()} and {@link
|
||||
* SpanData#getTotalAttributeCount} APIs.
|
||||
*
|
||||
* <p>See https://github.com/open-telemetry/opentelemetry-specification/issues/1089 for more
|
||||
* context on this approach.
|
||||
*/
|
||||
private static SpanData wrapSpanWithAttributes(SpanData span, Attributes attributes) {
|
||||
Attributes originalAttributes = span.getAttributes();
|
||||
Attributes replacementAttributes = originalAttributes.toBuilder().putAll(attributes).build();
|
||||
|
||||
int newAttributeKeyCount = 0;
|
||||
for (Entry<AttributeKey<?>, Object> entry : attributes.asMap().entrySet()) {
|
||||
if (originalAttributes.get(entry.getKey()) == null) {
|
||||
newAttributeKeyCount++;
|
||||
}
|
||||
}
|
||||
int originalTotalAttributeCount = span.getTotalAttributeCount();
|
||||
int replacementTotalAttributeCount = originalTotalAttributeCount + newAttributeKeyCount;
|
||||
|
||||
return new DelegatingSpanData(span) {
|
||||
@Override
|
||||
public Attributes getAttributes() {
|
||||
return replacementAttributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalAttributeCount() {
|
||||
return replacementTotalAttributeCount;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.contrib.awsxray;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import io.opentelemetry.sdk.resources.Resource;
|
||||
import io.opentelemetry.sdk.trace.export.SpanExporter;
|
||||
|
||||
public class AwsMetricAttributesSpanExporterBuilder {
|
||||
|
||||
// Defaults
|
||||
private static final MetricAttributeGenerator DEFAULT_GENERATOR =
|
||||
new AwsMetricAttributeGenerator();
|
||||
|
||||
// Required builder elements
|
||||
private final SpanExporter delegate;
|
||||
private final Resource resource;
|
||||
|
||||
// Optional builder elements
|
||||
private MetricAttributeGenerator generator = DEFAULT_GENERATOR;
|
||||
|
||||
public static AwsMetricAttributesSpanExporterBuilder create(
|
||||
SpanExporter delegate, Resource resource) {
|
||||
return new AwsMetricAttributesSpanExporterBuilder(delegate, resource);
|
||||
}
|
||||
|
||||
private AwsMetricAttributesSpanExporterBuilder(SpanExporter delegate, Resource resource) {
|
||||
this.delegate = delegate;
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the generator used to generate attributes used spancs exported by the exporter. If unset,
|
||||
* defaults to {@link #DEFAULT_GENERATOR}. Must not be null.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public AwsMetricAttributesSpanExporterBuilder setGenerator(MetricAttributeGenerator generator) {
|
||||
requireNonNull(generator, "generator");
|
||||
this.generator = generator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AwsMetricAttributesSpanExporter build() {
|
||||
return AwsMetricAttributesSpanExporter.create(delegate, generator, resource);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.contrib.awsxray;
|
||||
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE;
|
||||
|
||||
import io.opentelemetry.api.common.Attributes;
|
||||
import io.opentelemetry.api.metrics.DoubleHistogram;
|
||||
import io.opentelemetry.api.metrics.LongCounter;
|
||||
import io.opentelemetry.context.Context;
|
||||
import io.opentelemetry.sdk.resources.Resource;
|
||||
import io.opentelemetry.sdk.trace.ReadWriteSpan;
|
||||
import io.opentelemetry.sdk.trace.ReadableSpan;
|
||||
import io.opentelemetry.sdk.trace.SpanProcessor;
|
||||
import io.opentelemetry.sdk.trace.data.SpanData;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
/**
|
||||
* This processor will generate metrics based on span data. It depends on a {@link
|
||||
* MetricAttributeGenerator} being provided on instantiation, which will provide a means to
|
||||
* determine attributes which should be used to create metrics. A {@link Resource} must also be
|
||||
* provided, which is used to generate metrics. Finally, two {@link LongCounter}'s and a {@link
|
||||
* DoubleHistogram} must be provided, which will be used to actually create desired metrics (see
|
||||
* below)
|
||||
*
|
||||
* <p>AwsSpanMetricsProcessor produces metrics for errors (e.g. HTTP 4XX status codes), faults (e.g.
|
||||
* HTTP 5XX status codes), and latency (in Milliseconds). Errors and faults are counted, while
|
||||
* latency is measured with a histogram. Metrics are emitted with attributes derived from span
|
||||
* attributes.
|
||||
*
|
||||
* <p>For highest fidelity metrics, this processor should be coupled with the {@link
|
||||
* AlwaysRecordSampler}, which will result in 100% of spans being sent to the processor.
|
||||
*/
|
||||
@Immutable
|
||||
public final class AwsSpanMetricsProcessor implements SpanProcessor {
|
||||
|
||||
private static final double NANOS_TO_MILLIS = 1_000_000.0;
|
||||
|
||||
// Constants for deriving error and fault metrics
|
||||
private static final int ERROR_CODE_LOWER_BOUND = 400;
|
||||
private static final int ERROR_CODE_UPPER_BOUND = 499;
|
||||
private static final int FAULT_CODE_LOWER_BOUND = 500;
|
||||
private static final int FAULT_CODE_UPPER_BOUND = 599;
|
||||
|
||||
// Metric instruments
|
||||
private final LongCounter errorCounter;
|
||||
private final LongCounter faultCounter;
|
||||
private final DoubleHistogram latencyHistogram;
|
||||
|
||||
private final MetricAttributeGenerator generator;
|
||||
private final Resource resource;
|
||||
|
||||
/** Use {@link AwsSpanMetricsProcessorBuilder} to construct this processor. */
|
||||
static AwsSpanMetricsProcessor create(
|
||||
LongCounter errorCounter,
|
||||
LongCounter faultCounter,
|
||||
DoubleHistogram latencyHistogram,
|
||||
MetricAttributeGenerator generator,
|
||||
Resource resource) {
|
||||
return new AwsSpanMetricsProcessor(
|
||||
errorCounter, faultCounter, latencyHistogram, generator, resource);
|
||||
}
|
||||
|
||||
private AwsSpanMetricsProcessor(
|
||||
LongCounter errorCounter,
|
||||
LongCounter faultCounter,
|
||||
DoubleHistogram latencyHistogram,
|
||||
MetricAttributeGenerator generator,
|
||||
Resource resource) {
|
||||
this.errorCounter = errorCounter;
|
||||
this.faultCounter = faultCounter;
|
||||
this.latencyHistogram = latencyHistogram;
|
||||
this.generator = generator;
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart(Context parentContext, ReadWriteSpan span) {}
|
||||
|
||||
@Override
|
||||
public boolean isStartRequired() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnd(ReadableSpan span) {
|
||||
SpanData spanData = span.toSpanData();
|
||||
Attributes attributes = generator.generateMetricAttributesFromSpan(spanData, resource);
|
||||
|
||||
// Only record metrics if non-empty attributes are returned.
|
||||
if (!attributes.isEmpty()) {
|
||||
recordErrorOrFault(span, attributes);
|
||||
recordLatency(span, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEndRequired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void recordErrorOrFault(ReadableSpan span, Attributes attributes) {
|
||||
Long httpStatusCode = span.getAttribute(HTTP_STATUS_CODE);
|
||||
if (httpStatusCode == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (httpStatusCode >= ERROR_CODE_LOWER_BOUND && httpStatusCode <= ERROR_CODE_UPPER_BOUND) {
|
||||
errorCounter.add(1, attributes);
|
||||
} else if (httpStatusCode >= FAULT_CODE_LOWER_BOUND
|
||||
&& httpStatusCode <= FAULT_CODE_UPPER_BOUND) {
|
||||
faultCounter.add(1, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
private void recordLatency(ReadableSpan span, Attributes attributes) {
|
||||
long nanos = span.getLatencyNanos();
|
||||
double millis = nanos / NANOS_TO_MILLIS;
|
||||
latencyHistogram.record(millis, attributes);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.contrib.awsxray;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import io.opentelemetry.api.metrics.DoubleHistogram;
|
||||
import io.opentelemetry.api.metrics.LongCounter;
|
||||
import io.opentelemetry.api.metrics.Meter;
|
||||
import io.opentelemetry.api.metrics.MeterProvider;
|
||||
import io.opentelemetry.sdk.resources.Resource;
|
||||
|
||||
/** A builder for {@link AwsSpanMetricsProcessor} */
|
||||
public final class AwsSpanMetricsProcessorBuilder {
|
||||
|
||||
// Metric instrument configuration constants
|
||||
private static final String ERROR = "Error";
|
||||
private static final String FAULT = "Fault";
|
||||
private static final String LATENCY = "Latency";
|
||||
private static final String LATENCY_UNITS = "Milliseconds";
|
||||
|
||||
// Defaults
|
||||
private static final MetricAttributeGenerator DEFAULT_GENERATOR =
|
||||
new AwsMetricAttributeGenerator();
|
||||
private static final String DEFAULT_SCOPE_NAME = "AwsSpanMetricsProcessor";
|
||||
|
||||
// Required builder elements
|
||||
private final MeterProvider meterProvider;
|
||||
private final Resource resource;
|
||||
|
||||
// Optional builder elements
|
||||
private MetricAttributeGenerator generator = DEFAULT_GENERATOR;
|
||||
private String scopeName = DEFAULT_SCOPE_NAME;
|
||||
|
||||
public static AwsSpanMetricsProcessorBuilder create(
|
||||
MeterProvider meterProvider, Resource resource) {
|
||||
return new AwsSpanMetricsProcessorBuilder(meterProvider, resource);
|
||||
}
|
||||
|
||||
private AwsSpanMetricsProcessorBuilder(MeterProvider meterProvider, Resource resource) {
|
||||
this.meterProvider = meterProvider;
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the generator used to generate attributes used in metrics produced by span metrics
|
||||
* processor. If unset, defaults to {@link #DEFAULT_GENERATOR}. Must not be null.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public AwsSpanMetricsProcessorBuilder setGenerator(MetricAttributeGenerator generator) {
|
||||
requireNonNull(generator, "generator");
|
||||
this.generator = generator;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the scope name used in the creation of metrics by the span metrics processor. If unset,
|
||||
* defaults to {@link #DEFAULT_SCOPE_NAME}. Must not be null.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public AwsSpanMetricsProcessorBuilder setScopeName(String scopeName) {
|
||||
requireNonNull(scopeName, "scopeName");
|
||||
this.scopeName = scopeName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AwsSpanMetricsProcessor build() {
|
||||
Meter meter = meterProvider.get(scopeName);
|
||||
LongCounter errorCounter = meter.counterBuilder(ERROR).build();
|
||||
LongCounter faultCounter = meter.counterBuilder(FAULT).build();
|
||||
DoubleHistogram latencyHistogram =
|
||||
meter.histogramBuilder(LATENCY).setUnit(LATENCY_UNITS).build();
|
||||
|
||||
return AwsSpanMetricsProcessor.create(
|
||||
errorCounter, faultCounter, latencyHistogram, generator, resource);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.contrib.awsxray;
|
||||
|
||||
import io.opentelemetry.api.common.Attributes;
|
||||
import io.opentelemetry.sdk.resources.Resource;
|
||||
import io.opentelemetry.sdk.trace.data.SpanData;
|
||||
|
||||
/**
|
||||
* Metric attribute generator defines an interface for classes that can generate specific attributes
|
||||
* to be used by an {@link AwsSpanMetricsProcessor} to produce metrics and by {@link
|
||||
* AwsMetricAttributesSpanExporter} to wrap the original span.
|
||||
*/
|
||||
public interface MetricAttributeGenerator {
|
||||
|
||||
/**
|
||||
* Given a span and associated resource, produce meaningful metric attributes for metrics produced
|
||||
* from the span. If no metrics should be generated from this span, return {@link
|
||||
* Attributes#empty()}.
|
||||
*
|
||||
* @param span - SpanData to be used to generate metric attributes.
|
||||
* @param resource - Resource associated with Span to be used to generate metric attributes.
|
||||
* @return A set of zero or more attributes. Must not return null.
|
||||
*/
|
||||
Attributes generateMetricAttributesFromSpan(SpanData span, Resource resource);
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.contrib.awsxray;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.opentelemetry.api.common.AttributeKey;
|
||||
import io.opentelemetry.api.common.Attributes;
|
||||
import io.opentelemetry.api.trace.SpanKind;
|
||||
import io.opentelemetry.api.trace.TraceId;
|
||||
import io.opentelemetry.api.trace.TraceState;
|
||||
import io.opentelemetry.context.Context;
|
||||
import io.opentelemetry.sdk.trace.samplers.Sampler;
|
||||
import io.opentelemetry.sdk.trace.samplers.SamplingDecision;
|
||||
import io.opentelemetry.sdk.trace.samplers.SamplingResult;
|
||||
import java.util.Collections;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link AlwaysRecordSampler}. */
|
||||
class AlwaysRecordSamplerTest {
|
||||
|
||||
// Mocks
|
||||
private Sampler mockSampler;
|
||||
|
||||
private AlwaysRecordSampler sampler;
|
||||
|
||||
@BeforeEach
|
||||
void setUpSamplers() {
|
||||
mockSampler = mock(Sampler.class);
|
||||
sampler = AlwaysRecordSampler.create(mockSampler);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDescription() {
|
||||
when(mockSampler.getDescription()).thenReturn("mockDescription");
|
||||
assertThat(sampler.getDescription()).isEqualTo("AlwaysRecordSampler{mockDescription}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordAndSampleSamplingDecision() {
|
||||
validateShouldSample(SamplingDecision.RECORD_AND_SAMPLE, SamplingDecision.RECORD_AND_SAMPLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordOnlySamplingDecision() {
|
||||
validateShouldSample(SamplingDecision.RECORD_ONLY, SamplingDecision.RECORD_ONLY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDropSamplingDecision() {
|
||||
validateShouldSample(SamplingDecision.DROP, SamplingDecision.RECORD_ONLY);
|
||||
}
|
||||
|
||||
private void validateShouldSample(
|
||||
SamplingDecision rootDecision, SamplingDecision expectedDecision) {
|
||||
SamplingResult rootResult = buildRootSamplingResult(rootDecision);
|
||||
when(mockSampler.shouldSample(any(), anyString(), anyString(), any(), any(), any()))
|
||||
.thenReturn(rootResult);
|
||||
SamplingResult actualResult =
|
||||
sampler.shouldSample(
|
||||
Context.current(),
|
||||
TraceId.fromLongs(1, 2),
|
||||
"name",
|
||||
SpanKind.CLIENT,
|
||||
Attributes.empty(),
|
||||
Collections.emptyList());
|
||||
|
||||
if (rootDecision.equals(expectedDecision)) {
|
||||
assertThat(actualResult).isEqualTo(rootResult);
|
||||
assertThat(actualResult.getDecision()).isEqualTo(rootDecision);
|
||||
} else {
|
||||
assertThat(actualResult).isNotEqualTo(rootResult);
|
||||
assertThat(actualResult.getDecision()).isEqualTo(expectedDecision);
|
||||
}
|
||||
|
||||
assertThat(actualResult.getAttributes()).isEqualTo(rootResult.getAttributes());
|
||||
TraceState traceState = TraceState.builder().build();
|
||||
assertThat(actualResult.getUpdatedTraceState(traceState))
|
||||
.isEqualTo(rootResult.getUpdatedTraceState(traceState));
|
||||
}
|
||||
|
||||
private static SamplingResult buildRootSamplingResult(SamplingDecision samplingDecision) {
|
||||
return new SamplingResult() {
|
||||
@Override
|
||||
public SamplingDecision getDecision() {
|
||||
return samplingDecision;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Attributes getAttributes() {
|
||||
return Attributes.of(AttributeKey.stringKey("key"), samplingDecision.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TraceState getUpdatedTraceState(TraceState parentTraceState) {
|
||||
return TraceState.builder().put("key", samplingDecision.name()).build();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,319 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.contrib.awsxray;
|
||||
|
||||
import static io.opentelemetry.contrib.awsxray.AwsAttributeKeys.AWS_LOCAL_OPERATION;
|
||||
import static io.opentelemetry.contrib.awsxray.AwsAttributeKeys.AWS_LOCAL_SERVICE;
|
||||
import static io.opentelemetry.contrib.awsxray.AwsAttributeKeys.AWS_REMOTE_OPERATION;
|
||||
import static io.opentelemetry.contrib.awsxray.AwsAttributeKeys.AWS_REMOTE_SERVICE;
|
||||
import static io.opentelemetry.contrib.awsxray.AwsAttributeKeys.AWS_SPAN_KIND;
|
||||
import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_OPERATION;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_SYSTEM;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.FAAS_INVOKED_NAME;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.FAAS_INVOKED_PROVIDER;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.GRAPHQL_OPERATION_TYPE;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_OPERATION;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.MESSAGING_SYSTEM;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.PEER_SERVICE;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.RPC_METHOD;
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.RPC_SERVICE;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.opentelemetry.api.common.AttributeKey;
|
||||
import io.opentelemetry.api.common.Attributes;
|
||||
import io.opentelemetry.api.trace.SpanContext;
|
||||
import io.opentelemetry.api.trace.SpanKind;
|
||||
import io.opentelemetry.sdk.resources.Resource;
|
||||
import io.opentelemetry.sdk.trace.data.SpanData;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link AwsMetricAttributeGenerator}. */
|
||||
class AwsMetricAttributeGeneratorTest {
|
||||
|
||||
private static final AwsMetricAttributeGenerator GENERATOR = new AwsMetricAttributeGenerator();
|
||||
|
||||
// String constants that are used many times in these tests.
|
||||
private static final String AWS_LOCAL_OPERATION_VALUE = "AWS local operation";
|
||||
private static final String AWS_REMOTE_SERVICE_VALUE = "AWS remote service";
|
||||
private static final String AWS_REMOTE_OPERATION_VALUE = "AWS remote operation";
|
||||
private static final String SERVICE_NAME_VALUE = "Service name";
|
||||
private static final String SPAN_NAME_VALUE = "Span name";
|
||||
private static final String UNKNOWN_SERVICE = "UnknownService";
|
||||
private static final String UNKNOWN_OPERATION = "UnknownOperation";
|
||||
private static final String UNKNOWN_REMOTE_SERVICE = "UnknownRemoteService";
|
||||
private static final String UNKNOWN_REMOTE_OPERATION = "UnknownRemoteOperation";
|
||||
|
||||
private Attributes attributesMock;
|
||||
private SpanData spanDataMock;
|
||||
private Resource resource;
|
||||
|
||||
@BeforeEach
|
||||
public void setUpMocks() {
|
||||
attributesMock = mock(Attributes.class);
|
||||
spanDataMock = mock(SpanData.class);
|
||||
when(spanDataMock.getAttributes()).thenReturn(attributesMock);
|
||||
when(spanDataMock.getSpanContext()).thenReturn(mock(SpanContext.class));
|
||||
|
||||
resource = Resource.empty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConsumerSpanWithoutAttributes() {
|
||||
Attributes expectedAttributes =
|
||||
Attributes.of(
|
||||
AWS_SPAN_KIND, SpanKind.CONSUMER.name(),
|
||||
AWS_LOCAL_SERVICE, UNKNOWN_SERVICE,
|
||||
AWS_LOCAL_OPERATION, UNKNOWN_OPERATION);
|
||||
validateAttributesProducedForSpanOfKind(expectedAttributes, SpanKind.CONSUMER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testServerSpanWithoutAttributes() {
|
||||
Attributes expectedAttributes =
|
||||
Attributes.of(
|
||||
AWS_SPAN_KIND, SpanKind.SERVER.name(),
|
||||
AWS_LOCAL_SERVICE, UNKNOWN_SERVICE,
|
||||
AWS_LOCAL_OPERATION, UNKNOWN_OPERATION);
|
||||
validateAttributesProducedForSpanOfKind(expectedAttributes, SpanKind.SERVER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProducerSpanWithoutAttributes() {
|
||||
Attributes expectedAttributes =
|
||||
Attributes.of(
|
||||
AWS_SPAN_KIND, SpanKind.PRODUCER.name(),
|
||||
AWS_LOCAL_SERVICE, UNKNOWN_SERVICE,
|
||||
AWS_LOCAL_OPERATION, UNKNOWN_OPERATION,
|
||||
AWS_REMOTE_SERVICE, UNKNOWN_REMOTE_SERVICE,
|
||||
AWS_REMOTE_OPERATION, UNKNOWN_REMOTE_OPERATION);
|
||||
validateAttributesProducedForSpanOfKind(expectedAttributes, SpanKind.PRODUCER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClientSpanWithoutAttributes() {
|
||||
Attributes expectedAttributes =
|
||||
Attributes.of(
|
||||
AWS_SPAN_KIND, SpanKind.CLIENT.name(),
|
||||
AWS_LOCAL_SERVICE, UNKNOWN_SERVICE,
|
||||
AWS_LOCAL_OPERATION, UNKNOWN_OPERATION,
|
||||
AWS_REMOTE_SERVICE, UNKNOWN_REMOTE_SERVICE,
|
||||
AWS_REMOTE_OPERATION, UNKNOWN_REMOTE_OPERATION);
|
||||
validateAttributesProducedForSpanOfKind(expectedAttributes, SpanKind.CLIENT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInternalSpan() {
|
||||
// Spans with internal span kind should not produce any attributes.
|
||||
validateAttributesProducedForSpanOfKind(Attributes.empty(), SpanKind.INTERNAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConsumerSpanWithAttributes() {
|
||||
updateResourceWithServiceName();
|
||||
when(spanDataMock.getName()).thenReturn(SPAN_NAME_VALUE);
|
||||
|
||||
Attributes expectedAttributes =
|
||||
Attributes.of(
|
||||
AWS_SPAN_KIND, SpanKind.CONSUMER.name(),
|
||||
AWS_LOCAL_SERVICE, SERVICE_NAME_VALUE,
|
||||
AWS_LOCAL_OPERATION, SPAN_NAME_VALUE);
|
||||
validateAttributesProducedForSpanOfKind(expectedAttributes, SpanKind.CONSUMER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testServerSpanWithAttributes() {
|
||||
updateResourceWithServiceName();
|
||||
when(spanDataMock.getName()).thenReturn(SPAN_NAME_VALUE);
|
||||
|
||||
Attributes expectedAttributes =
|
||||
Attributes.of(
|
||||
AWS_SPAN_KIND, SpanKind.SERVER.name(),
|
||||
AWS_LOCAL_SERVICE, SERVICE_NAME_VALUE,
|
||||
AWS_LOCAL_OPERATION, SPAN_NAME_VALUE);
|
||||
validateAttributesProducedForSpanOfKind(expectedAttributes, SpanKind.SERVER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProducerSpanWithAttributes() {
|
||||
updateResourceWithServiceName();
|
||||
mockAttribute(AWS_LOCAL_OPERATION, AWS_LOCAL_OPERATION_VALUE);
|
||||
mockAttribute(AWS_REMOTE_SERVICE, AWS_REMOTE_SERVICE_VALUE);
|
||||
mockAttribute(AWS_REMOTE_OPERATION, AWS_REMOTE_OPERATION_VALUE);
|
||||
|
||||
Attributes expectedAttributes =
|
||||
Attributes.of(
|
||||
AWS_SPAN_KIND, SpanKind.PRODUCER.name(),
|
||||
AWS_LOCAL_SERVICE, SERVICE_NAME_VALUE,
|
||||
AWS_LOCAL_OPERATION, AWS_LOCAL_OPERATION_VALUE,
|
||||
AWS_REMOTE_SERVICE, AWS_REMOTE_SERVICE_VALUE,
|
||||
AWS_REMOTE_OPERATION, AWS_REMOTE_OPERATION_VALUE);
|
||||
validateAttributesProducedForSpanOfKind(expectedAttributes, SpanKind.PRODUCER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClientSpanWithAttributes() {
|
||||
updateResourceWithServiceName();
|
||||
mockAttribute(AWS_LOCAL_OPERATION, AWS_LOCAL_OPERATION_VALUE);
|
||||
mockAttribute(AWS_REMOTE_SERVICE, AWS_REMOTE_SERVICE_VALUE);
|
||||
mockAttribute(AWS_REMOTE_OPERATION, AWS_REMOTE_OPERATION_VALUE);
|
||||
|
||||
Attributes expectedAttributes =
|
||||
Attributes.of(
|
||||
AWS_SPAN_KIND, SpanKind.CLIENT.name(),
|
||||
AWS_LOCAL_SERVICE, SERVICE_NAME_VALUE,
|
||||
AWS_LOCAL_OPERATION, AWS_LOCAL_OPERATION_VALUE,
|
||||
AWS_REMOTE_SERVICE, AWS_REMOTE_SERVICE_VALUE,
|
||||
AWS_REMOTE_OPERATION, AWS_REMOTE_OPERATION_VALUE);
|
||||
validateAttributesProducedForSpanOfKind(expectedAttributes, SpanKind.CLIENT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoteAttributesCombinations() {
|
||||
// Set all expected fields to a test string, we will overwrite them in descending order to test
|
||||
// the priority-order logic in AwsMetricAttributeGenerator remote attribute methods.
|
||||
mockAttribute(AWS_REMOTE_SERVICE, "TestString");
|
||||
mockAttribute(AWS_REMOTE_OPERATION, "TestString");
|
||||
mockAttribute(RPC_SERVICE, "TestString");
|
||||
mockAttribute(RPC_METHOD, "TestString");
|
||||
mockAttribute(DB_SYSTEM, "TestString");
|
||||
mockAttribute(DB_OPERATION, "TestString");
|
||||
mockAttribute(FAAS_INVOKED_PROVIDER, "TestString");
|
||||
mockAttribute(FAAS_INVOKED_NAME, "TestString");
|
||||
mockAttribute(MESSAGING_SYSTEM, "TestString");
|
||||
mockAttribute(MESSAGING_OPERATION, "TestString");
|
||||
mockAttribute(GRAPHQL_OPERATION_TYPE, "TestString");
|
||||
// Do not set dummy value for PEER_SERVICE, since it has special behaviour.
|
||||
|
||||
// Two unused attributes to show that we will not make use of unrecognized attributes
|
||||
mockAttribute(AttributeKey.stringKey("unknown.service.key"), "TestString");
|
||||
mockAttribute(AttributeKey.stringKey("unknown.operation.key"), "TestString");
|
||||
|
||||
// Validate behaviour of various combinations of AWS remote attributes, then remove them.
|
||||
validateAndRemoveRemoteAttributes(
|
||||
AWS_REMOTE_SERVICE,
|
||||
AWS_REMOTE_SERVICE_VALUE,
|
||||
AWS_REMOTE_OPERATION,
|
||||
AWS_REMOTE_OPERATION_VALUE);
|
||||
|
||||
// Validate behaviour of various combinations of RPC attributes, then remove them.
|
||||
validateAndRemoveRemoteAttributes(RPC_SERVICE, "RPC service", RPC_METHOD, "RPC method");
|
||||
|
||||
// Validate behaviour of various combinations of DB attributes, then remove them.
|
||||
validateAndRemoveRemoteAttributes(DB_SYSTEM, "DB system", DB_OPERATION, "DB operation");
|
||||
|
||||
// Validate behaviour of various combinations of FAAS attributes, then remove them.
|
||||
validateAndRemoveRemoteAttributes(
|
||||
FAAS_INVOKED_PROVIDER, "FAAS invoked provider", FAAS_INVOKED_NAME, "FAAS invoked name");
|
||||
|
||||
// Validate behaviour of various combinations of Messaging attributes, then remove them.
|
||||
validateAndRemoveRemoteAttributes(
|
||||
MESSAGING_SYSTEM, "Messaging system", MESSAGING_OPERATION, "Messaging operation");
|
||||
|
||||
// Validate behaviour of GraphQL operation type attribute, then remove it.
|
||||
mockAttribute(GRAPHQL_OPERATION_TYPE, "GraphQL operation type");
|
||||
validateExpectedRemoteAttributes("graphql", "GraphQL operation type");
|
||||
mockAttribute(GRAPHQL_OPERATION_TYPE, null);
|
||||
|
||||
// Validate behaviour of Peer service attribute, then remove it.
|
||||
mockAttribute(PEER_SERVICE, "Peer service");
|
||||
validateExpectedRemoteAttributes("Peer service", UNKNOWN_REMOTE_OPERATION);
|
||||
mockAttribute(PEER_SERVICE, null);
|
||||
|
||||
// Once we have removed all usable metrics, we only have "unknown" attributes, which are unused.
|
||||
validateExpectedRemoteAttributes(UNKNOWN_REMOTE_SERVICE, UNKNOWN_REMOTE_OPERATION);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPeerServiceDoesOverrideOtherRemoteServices() {
|
||||
validatePeerServiceDoesOverride(RPC_SERVICE);
|
||||
validatePeerServiceDoesOverride(DB_SYSTEM);
|
||||
validatePeerServiceDoesOverride(FAAS_INVOKED_PROVIDER);
|
||||
validatePeerServiceDoesOverride(MESSAGING_SYSTEM);
|
||||
validatePeerServiceDoesOverride(GRAPHQL_OPERATION_TYPE);
|
||||
// Actually testing that peer service overrides "UnknownRemoteService".
|
||||
validatePeerServiceDoesOverride(AttributeKey.stringKey("unknown.service.key"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPeerServiceDoesNotOverrideAwsRemoteService() {
|
||||
mockAttribute(AWS_REMOTE_SERVICE, "TestString");
|
||||
mockAttribute(PEER_SERVICE, "PeerService");
|
||||
|
||||
when(spanDataMock.getKind()).thenReturn(SpanKind.CLIENT);
|
||||
Attributes actualAttributes =
|
||||
GENERATOR.generateMetricAttributesFromSpan(spanDataMock, resource);
|
||||
assertThat(actualAttributes.get(AWS_REMOTE_SERVICE)).isEqualTo("TestString");
|
||||
}
|
||||
|
||||
private void mockAttribute(AttributeKey<String> key, String value) {
|
||||
when(attributesMock.get(key)).thenReturn(value);
|
||||
}
|
||||
|
||||
private void validateAttributesProducedForSpanOfKind(
|
||||
Attributes expectedAttributes, SpanKind kind) {
|
||||
when(spanDataMock.getKind()).thenReturn(kind);
|
||||
Attributes actualAttributes =
|
||||
GENERATOR.generateMetricAttributesFromSpan(spanDataMock, resource);
|
||||
assertThat(actualAttributes).isEqualTo(expectedAttributes);
|
||||
}
|
||||
|
||||
private void updateResourceWithServiceName() {
|
||||
resource = Resource.builder().put(SERVICE_NAME, SERVICE_NAME_VALUE).build();
|
||||
}
|
||||
|
||||
private void validateExpectedRemoteAttributes(
|
||||
String expectedRemoteService, String expectedRemoteOperation) {
|
||||
when(spanDataMock.getKind()).thenReturn(SpanKind.CLIENT);
|
||||
Attributes actualAttributes =
|
||||
GENERATOR.generateMetricAttributesFromSpan(spanDataMock, resource);
|
||||
assertThat(actualAttributes.get(AWS_REMOTE_SERVICE)).isEqualTo(expectedRemoteService);
|
||||
assertThat(actualAttributes.get(AWS_REMOTE_OPERATION)).isEqualTo(expectedRemoteOperation);
|
||||
|
||||
when(spanDataMock.getKind()).thenReturn(SpanKind.PRODUCER);
|
||||
actualAttributes = GENERATOR.generateMetricAttributesFromSpan(spanDataMock, resource);
|
||||
assertThat(actualAttributes.get(AWS_REMOTE_SERVICE)).isEqualTo(expectedRemoteService);
|
||||
assertThat(actualAttributes.get(AWS_REMOTE_OPERATION)).isEqualTo(expectedRemoteOperation);
|
||||
}
|
||||
|
||||
private void validateAndRemoveRemoteAttributes(
|
||||
AttributeKey<String> remoteServiceKey,
|
||||
String remoteServiceValue,
|
||||
AttributeKey<String> remoteOperationKey,
|
||||
String remoteOperationValue) {
|
||||
mockAttribute(remoteServiceKey, remoteServiceValue);
|
||||
mockAttribute(remoteOperationKey, remoteOperationValue);
|
||||
validateExpectedRemoteAttributes(remoteServiceValue, remoteOperationValue);
|
||||
|
||||
mockAttribute(remoteServiceKey, null);
|
||||
mockAttribute(remoteOperationKey, remoteOperationValue);
|
||||
validateExpectedRemoteAttributes(UNKNOWN_REMOTE_SERVICE, remoteOperationValue);
|
||||
|
||||
mockAttribute(remoteServiceKey, remoteServiceValue);
|
||||
mockAttribute(remoteOperationKey, null);
|
||||
validateExpectedRemoteAttributes(remoteServiceValue, UNKNOWN_REMOTE_OPERATION);
|
||||
|
||||
mockAttribute(remoteServiceKey, null);
|
||||
mockAttribute(remoteOperationKey, null);
|
||||
}
|
||||
|
||||
private void validatePeerServiceDoesOverride(AttributeKey<String> remoteServiceKey) {
|
||||
mockAttribute(remoteServiceKey, "TestString");
|
||||
mockAttribute(PEER_SERVICE, "PeerService");
|
||||
|
||||
// Validate that peer service value takes precedence over whatever remoteServiceKey was set
|
||||
when(spanDataMock.getKind()).thenReturn(SpanKind.CLIENT);
|
||||
Attributes actualAttributes =
|
||||
GENERATOR.generateMetricAttributesFromSpan(spanDataMock, resource);
|
||||
assertThat(actualAttributes.get(AWS_REMOTE_SERVICE)).isEqualTo("PeerService");
|
||||
|
||||
mockAttribute(remoteServiceKey, null);
|
||||
mockAttribute(PEER_SERVICE, null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,320 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.contrib.awsxray;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.opentelemetry.api.common.AttributeKey;
|
||||
import io.opentelemetry.api.common.Attributes;
|
||||
import io.opentelemetry.api.trace.SpanContext;
|
||||
import io.opentelemetry.api.trace.SpanKind;
|
||||
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
|
||||
import io.opentelemetry.sdk.resources.Resource;
|
||||
import io.opentelemetry.sdk.trace.data.EventData;
|
||||
import io.opentelemetry.sdk.trace.data.LinkData;
|
||||
import io.opentelemetry.sdk.trace.data.SpanData;
|
||||
import io.opentelemetry.sdk.trace.data.StatusData;
|
||||
import io.opentelemetry.sdk.trace.export.SpanExporter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
/** Unit tests for {@link AwsSpanMetricsProcessor}. */
|
||||
class AwsMetricAttributesSpanExporterTest {
|
||||
|
||||
@Captor private static ArgumentCaptor<Collection<SpanData>> delegateExportCaptor;
|
||||
|
||||
// Test constants
|
||||
private static final boolean CONTAINS_ATTRIBUTES = true;
|
||||
private static final boolean CONTAINS_NO_ATTRIBUTES = false;
|
||||
|
||||
// Resource is not mockable, but tests can safely rely on an empty resource.
|
||||
private static final Resource testResource = Resource.empty();
|
||||
|
||||
// Mocks required for tests.
|
||||
private MetricAttributeGenerator generatorMock;
|
||||
private SpanExporter delegateMock;
|
||||
|
||||
private AwsMetricAttributesSpanExporter awsMetricAttributesSpanExporter;
|
||||
|
||||
@BeforeEach
|
||||
public void setUpMocks() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
generatorMock = mock(MetricAttributeGenerator.class);
|
||||
delegateMock = mock(SpanExporter.class);
|
||||
|
||||
awsMetricAttributesSpanExporter =
|
||||
AwsMetricAttributesSpanExporter.create(delegateMock, generatorMock, testResource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPassthroughDelegations() {
|
||||
awsMetricAttributesSpanExporter.flush();
|
||||
awsMetricAttributesSpanExporter.shutdown();
|
||||
awsMetricAttributesSpanExporter.close();
|
||||
verify(delegateMock, times(1)).flush();
|
||||
verify(delegateMock, times(1)).shutdown();
|
||||
verify(delegateMock, times(1)).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExportDelegationWithoutAttributeOrModification() {
|
||||
Attributes spanAttributes = buildSpanAttributes(CONTAINS_NO_ATTRIBUTES);
|
||||
SpanData spanDataMock = buildSpanDataMock(spanAttributes);
|
||||
Attributes metricAttributes = buildMetricAttributes(CONTAINS_NO_ATTRIBUTES);
|
||||
configureMocksForExport(spanDataMock, metricAttributes);
|
||||
|
||||
awsMetricAttributesSpanExporter.export(Collections.singletonList(spanDataMock));
|
||||
verify(delegateMock, times(1)).export(delegateExportCaptor.capture());
|
||||
Collection<SpanData> exportedSpans = delegateExportCaptor.getValue();
|
||||
assertThat(exportedSpans.size()).isEqualTo(1);
|
||||
|
||||
SpanData exportedSpan = (SpanData) exportedSpans.toArray()[0];
|
||||
assertThat(exportedSpan).isEqualTo(spanDataMock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExportDelegationWithAttributeButWithoutModification() {
|
||||
Attributes spanAttributes = buildSpanAttributes(CONTAINS_ATTRIBUTES);
|
||||
SpanData spanDataMock = buildSpanDataMock(spanAttributes);
|
||||
Attributes metricAttributes = buildMetricAttributes(CONTAINS_NO_ATTRIBUTES);
|
||||
configureMocksForExport(spanDataMock, metricAttributes);
|
||||
|
||||
awsMetricAttributesSpanExporter.export(Collections.singletonList(spanDataMock));
|
||||
verify(delegateMock, times(1)).export(delegateExportCaptor.capture());
|
||||
Collection<SpanData> exportedSpans = delegateExportCaptor.getValue();
|
||||
assertThat(exportedSpans.size()).isEqualTo(1);
|
||||
|
||||
SpanData exportedSpan = (SpanData) exportedSpans.toArray()[0];
|
||||
assertThat(exportedSpan).isEqualTo(spanDataMock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExportDelegationWithoutAttributeButWithModification() {
|
||||
Attributes spanAttributes = buildSpanAttributes(CONTAINS_NO_ATTRIBUTES);
|
||||
SpanData spanDataMock = buildSpanDataMock(spanAttributes);
|
||||
Attributes metricAttributes = buildMetricAttributes(CONTAINS_ATTRIBUTES);
|
||||
configureMocksForExport(spanDataMock, metricAttributes);
|
||||
|
||||
awsMetricAttributesSpanExporter.export(Collections.singletonList(spanDataMock));
|
||||
verify(delegateMock, times(1)).export(delegateExportCaptor.capture());
|
||||
List<SpanData> exportedSpans = (List<SpanData>) delegateExportCaptor.getValue();
|
||||
assertThat(exportedSpans.size()).isEqualTo(1);
|
||||
|
||||
SpanData exportedSpan = exportedSpans.get(0);
|
||||
assertThat(exportedSpan.getClass()).isNotEqualTo(spanDataMock.getClass());
|
||||
assertThat(exportedSpan.getTotalAttributeCount()).isEqualTo(metricAttributes.size());
|
||||
Attributes exportedAttributes = exportedSpan.getAttributes();
|
||||
assertThat(exportedAttributes.size()).isEqualTo(metricAttributes.size());
|
||||
metricAttributes.forEach((k, v) -> assertThat(exportedAttributes.get(k)).isEqualTo(v));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExportDelegationWithAttributeAndModification() {
|
||||
Attributes spanAttributes = buildSpanAttributes(CONTAINS_ATTRIBUTES);
|
||||
SpanData spanDataMock = buildSpanDataMock(spanAttributes);
|
||||
Attributes metricAttributes = buildMetricAttributes(CONTAINS_ATTRIBUTES);
|
||||
configureMocksForExport(spanDataMock, metricAttributes);
|
||||
|
||||
awsMetricAttributesSpanExporter.export(Collections.singletonList(spanDataMock));
|
||||
verify(delegateMock, times(1)).export(delegateExportCaptor.capture());
|
||||
List<SpanData> exportedSpans = (List<SpanData>) delegateExportCaptor.getValue();
|
||||
assertThat(exportedSpans.size()).isEqualTo(1);
|
||||
|
||||
SpanData exportedSpan = exportedSpans.get(0);
|
||||
assertThat(exportedSpan.getClass()).isNotEqualTo(spanDataMock.getClass());
|
||||
int expectedAttributeCount = metricAttributes.size() + spanAttributes.size();
|
||||
assertThat(exportedSpan.getTotalAttributeCount()).isEqualTo(expectedAttributeCount);
|
||||
Attributes exportedAttributes = exportedSpan.getAttributes();
|
||||
assertThat(exportedAttributes.size()).isEqualTo(expectedAttributeCount);
|
||||
spanAttributes.forEach((k, v) -> assertThat(exportedAttributes.get(k)).isEqualTo(v));
|
||||
metricAttributes.forEach((k, v) -> assertThat(exportedAttributes.get(k)).isEqualTo(v));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExportDelegationWithMultipleSpans() {
|
||||
Attributes spanAttributes1 = buildSpanAttributes(CONTAINS_NO_ATTRIBUTES);
|
||||
SpanData spanDataMock1 = buildSpanDataMock(spanAttributes1);
|
||||
Attributes metricAttributes1 = buildMetricAttributes(CONTAINS_NO_ATTRIBUTES);
|
||||
configureMocksForExport(spanDataMock1, metricAttributes1);
|
||||
|
||||
Attributes spanAttributes2 = buildSpanAttributes(CONTAINS_ATTRIBUTES);
|
||||
SpanData spanDataMock2 = buildSpanDataMock(spanAttributes2);
|
||||
Attributes metricAttributes2 = buildMetricAttributes(CONTAINS_ATTRIBUTES);
|
||||
configureMocksForExport(spanDataMock2, metricAttributes2);
|
||||
|
||||
Attributes spanAttributes3 = buildSpanAttributes(CONTAINS_ATTRIBUTES);
|
||||
SpanData spanDataMock3 = buildSpanDataMock(spanAttributes3);
|
||||
Attributes metricAttributes3 = buildMetricAttributes(CONTAINS_NO_ATTRIBUTES);
|
||||
configureMocksForExport(spanDataMock3, metricAttributes3);
|
||||
|
||||
awsMetricAttributesSpanExporter.export(
|
||||
Arrays.asList(spanDataMock1, spanDataMock2, spanDataMock3));
|
||||
verify(delegateMock, times(1)).export(delegateExportCaptor.capture());
|
||||
List<SpanData> exportedSpans = (List<SpanData>) delegateExportCaptor.getValue();
|
||||
assertThat(exportedSpans.size()).isEqualTo(3);
|
||||
|
||||
SpanData exportedSpan1 = exportedSpans.get(0);
|
||||
SpanData exportedSpan2 = exportedSpans.get(1);
|
||||
SpanData exportedSpan3 = exportedSpans.get(2);
|
||||
|
||||
assertThat(exportedSpan1).isEqualTo(spanDataMock1);
|
||||
assertThat(exportedSpan3).isEqualTo(spanDataMock3);
|
||||
|
||||
assertThat(exportedSpan2.getClass()).isNotEqualTo(spanDataMock2.getClass());
|
||||
int expectedAttributeCount = metricAttributes2.size() + spanAttributes2.size();
|
||||
assertThat(exportedSpan2.getTotalAttributeCount()).isEqualTo(expectedAttributeCount);
|
||||
Attributes exportedAttributes = exportedSpan2.getAttributes();
|
||||
assertThat(exportedAttributes.size()).isEqualTo(expectedAttributeCount);
|
||||
spanAttributes2.forEach((k, v) -> assertThat(exportedAttributes.get(k)).isEqualTo(v));
|
||||
metricAttributes2.forEach((k, v) -> assertThat(exportedAttributes.get(k)).isEqualTo(v));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOverridenAttributes() {
|
||||
Attributes spanAttributes =
|
||||
Attributes.of(
|
||||
AttributeKey.stringKey("key1"),
|
||||
"old value1",
|
||||
AttributeKey.stringKey("key2"),
|
||||
"old value2");
|
||||
SpanData spanDataMock = buildSpanDataMock(spanAttributes);
|
||||
Attributes metricAttributes =
|
||||
Attributes.of(
|
||||
AttributeKey.stringKey("key1"),
|
||||
"new value1",
|
||||
AttributeKey.stringKey("key3"),
|
||||
"new value3");
|
||||
configureMocksForExport(spanDataMock, metricAttributes);
|
||||
|
||||
awsMetricAttributesSpanExporter.export(Collections.singletonList(spanDataMock));
|
||||
verify(delegateMock, times(1)).export(delegateExportCaptor.capture());
|
||||
List<SpanData> exportedSpans = (List<SpanData>) delegateExportCaptor.getValue();
|
||||
assertThat(exportedSpans.size()).isEqualTo(1);
|
||||
|
||||
SpanData exportedSpan = exportedSpans.get(0);
|
||||
assertThat(exportedSpan.getClass()).isNotEqualTo(spanDataMock.getClass());
|
||||
assertThat(exportedSpan.getTotalAttributeCount()).isEqualTo(3);
|
||||
Attributes exportedAttributes = exportedSpan.getAttributes();
|
||||
assertThat(exportedAttributes.size()).isEqualTo(3);
|
||||
assertThat(exportedAttributes.get(AttributeKey.stringKey("key1"))).isEqualTo("new value1");
|
||||
assertThat(exportedAttributes.get(AttributeKey.stringKey("key2"))).isEqualTo("old value2");
|
||||
assertThat(exportedAttributes.get(AttributeKey.stringKey("key3"))).isEqualTo("new value3");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExportDelegatingSpanDataBehaviour() {
|
||||
Attributes spanAttributes = buildSpanAttributes(CONTAINS_ATTRIBUTES);
|
||||
SpanData spanDataMock = buildSpanDataMock(spanAttributes);
|
||||
Attributes metricAttributes = buildMetricAttributes(CONTAINS_ATTRIBUTES);
|
||||
configureMocksForExport(spanDataMock, metricAttributes);
|
||||
|
||||
awsMetricAttributesSpanExporter.export(Collections.singletonList(spanDataMock));
|
||||
verify(delegateMock, times(1)).export(delegateExportCaptor.capture());
|
||||
List<SpanData> exportedSpans = (List<SpanData>) delegateExportCaptor.getValue();
|
||||
assertThat(exportedSpans.size()).isEqualTo(1);
|
||||
|
||||
SpanData exportedSpan = exportedSpans.get(0);
|
||||
|
||||
SpanContext spanContextMock = mock(SpanContext.class);
|
||||
when(spanDataMock.getSpanContext()).thenReturn(spanContextMock);
|
||||
assertThat(exportedSpan.getSpanContext()).isEqualTo(spanContextMock);
|
||||
|
||||
SpanContext parentSpanContextMock = mock(SpanContext.class);
|
||||
when(spanDataMock.getParentSpanContext()).thenReturn(parentSpanContextMock);
|
||||
assertThat(exportedSpan.getParentSpanContext()).isEqualTo(parentSpanContextMock);
|
||||
|
||||
when(spanDataMock.getResource()).thenReturn(testResource);
|
||||
assertThat(exportedSpan.getResource()).isEqualTo(testResource);
|
||||
|
||||
// InstrumentationLibraryInfo is deprecated, so actually invoking it causes build failures.
|
||||
// Excluding from this test.
|
||||
|
||||
InstrumentationScopeInfo testInstrumentationScopeInfo = InstrumentationScopeInfo.empty();
|
||||
when(spanDataMock.getInstrumentationScopeInfo()).thenReturn(testInstrumentationScopeInfo);
|
||||
assertThat(exportedSpan.getInstrumentationScopeInfo()).isEqualTo(testInstrumentationScopeInfo);
|
||||
|
||||
String testName = "name";
|
||||
when(spanDataMock.getName()).thenReturn(testName);
|
||||
assertThat(exportedSpan.getName()).isEqualTo(testName);
|
||||
|
||||
SpanKind kindMock = mock(SpanKind.class);
|
||||
when(spanDataMock.getKind()).thenReturn(kindMock);
|
||||
assertThat(exportedSpan.getKind()).isEqualTo(kindMock);
|
||||
|
||||
long testStartEpochNanos = 1L;
|
||||
when(spanDataMock.getStartEpochNanos()).thenReturn(testStartEpochNanos);
|
||||
assertThat(exportedSpan.getStartEpochNanos()).isEqualTo(testStartEpochNanos);
|
||||
|
||||
List<EventData> eventsMock = Collections.singletonList(mock(EventData.class));
|
||||
when(spanDataMock.getEvents()).thenReturn(eventsMock);
|
||||
assertThat(exportedSpan.getEvents()).isEqualTo(eventsMock);
|
||||
|
||||
List<LinkData> linksMock = Collections.singletonList(mock(LinkData.class));
|
||||
when(spanDataMock.getLinks()).thenReturn(linksMock);
|
||||
assertThat(exportedSpan.getLinks()).isEqualTo(linksMock);
|
||||
|
||||
StatusData statusMock = mock(StatusData.class);
|
||||
when(spanDataMock.getStatus()).thenReturn(statusMock);
|
||||
assertThat(exportedSpan.getStatus()).isEqualTo(statusMock);
|
||||
|
||||
long testEndEpochNanosMock = 2L;
|
||||
when(spanDataMock.getEndEpochNanos()).thenReturn(testEndEpochNanosMock);
|
||||
assertThat(exportedSpan.getEndEpochNanos()).isEqualTo(testEndEpochNanosMock);
|
||||
|
||||
when(spanDataMock.hasEnded()).thenReturn(true);
|
||||
assertThat(exportedSpan.hasEnded()).isEqualTo(true);
|
||||
|
||||
int testTotalRecordedEventsMock = 3;
|
||||
when(spanDataMock.getTotalRecordedEvents()).thenReturn(testTotalRecordedEventsMock);
|
||||
assertThat(exportedSpan.getTotalRecordedEvents()).isEqualTo(testTotalRecordedEventsMock);
|
||||
|
||||
int testTotalRecordedLinksMock = 4;
|
||||
when(spanDataMock.getTotalRecordedLinks()).thenReturn(testTotalRecordedLinksMock);
|
||||
assertThat(exportedSpan.getTotalRecordedLinks()).isEqualTo(testTotalRecordedLinksMock);
|
||||
}
|
||||
|
||||
private static Attributes buildSpanAttributes(boolean containsAttribute) {
|
||||
if (containsAttribute) {
|
||||
return Attributes.of(AttributeKey.stringKey("original key"), "original value");
|
||||
} else {
|
||||
return Attributes.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static Attributes buildMetricAttributes(boolean containsAttribute) {
|
||||
if (containsAttribute) {
|
||||
return Attributes.of(AttributeKey.stringKey("new key"), "new value");
|
||||
} else {
|
||||
return Attributes.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static SpanData buildSpanDataMock(Attributes spanAttributes) {
|
||||
// Configure spanData
|
||||
SpanData mockSpanData = mock(SpanData.class);
|
||||
when(mockSpanData.getAttributes()).thenReturn(spanAttributes);
|
||||
when(mockSpanData.getTotalAttributeCount()).thenReturn(spanAttributes.size());
|
||||
return mockSpanData;
|
||||
}
|
||||
|
||||
private void configureMocksForExport(SpanData spanDataMock, Attributes metricAttributes) {
|
||||
// Configure generated attributes
|
||||
when(generatorMock.generateMetricAttributesFromSpan(eq(spanDataMock), eq(testResource)))
|
||||
.thenReturn(metricAttributes);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.contrib.awsxray;
|
||||
|
||||
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.opentelemetry.api.common.AttributeKey;
|
||||
import io.opentelemetry.api.common.Attributes;
|
||||
import io.opentelemetry.api.metrics.DoubleHistogram;
|
||||
import io.opentelemetry.api.metrics.LongCounter;
|
||||
import io.opentelemetry.context.Context;
|
||||
import io.opentelemetry.sdk.common.CompletableResultCode;
|
||||
import io.opentelemetry.sdk.resources.Resource;
|
||||
import io.opentelemetry.sdk.trace.ReadWriteSpan;
|
||||
import io.opentelemetry.sdk.trace.ReadableSpan;
|
||||
import io.opentelemetry.sdk.trace.data.SpanData;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link AwsSpanMetricsProcessor}. */
|
||||
class AwsSpanMetricsProcessorTest {
|
||||
|
||||
// Test constants
|
||||
private static final boolean CONTAINS_ATTRIBUTES = true;
|
||||
private static final boolean CONTAINS_NO_ATTRIBUTES = false;
|
||||
private static final double TEST_LATENCY_MILLIS = 150.0;
|
||||
private static final long TEST_LATENCY_NANOS = 150_000_000L;
|
||||
|
||||
// Resource is not mockable, but tests can safely rely on an empty resource.
|
||||
private static final Resource testResource = Resource.empty();
|
||||
|
||||
// Useful enum for indicating expected HTTP status code-related metrics
|
||||
private enum ExpectedStatusMetric {
|
||||
ERROR,
|
||||
FAULT,
|
||||
NEITHER
|
||||
}
|
||||
|
||||
// Mocks required for tests.
|
||||
private LongCounter errorCounterMock;
|
||||
private LongCounter faultCounterMock;
|
||||
private DoubleHistogram latencyHistogramMock;
|
||||
private MetricAttributeGenerator generatorMock;
|
||||
|
||||
private AwsSpanMetricsProcessor awsSpanMetricsProcessor;
|
||||
|
||||
@BeforeEach
|
||||
public void setUpMocks() {
|
||||
errorCounterMock = mock(LongCounter.class);
|
||||
faultCounterMock = mock(LongCounter.class);
|
||||
latencyHistogramMock = mock(DoubleHistogram.class);
|
||||
generatorMock = mock(MetricAttributeGenerator.class);
|
||||
|
||||
awsSpanMetricsProcessor =
|
||||
AwsSpanMetricsProcessor.create(
|
||||
errorCounterMock, faultCounterMock, latencyHistogramMock, generatorMock, testResource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsRequired() {
|
||||
assertThat(awsSpanMetricsProcessor.isStartRequired()).isFalse();
|
||||
assertThat(awsSpanMetricsProcessor.isEndRequired()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartDoesNothingToSpan() {
|
||||
Context parentContextMock = mock(Context.class);
|
||||
ReadWriteSpan spanMock = mock(ReadWriteSpan.class);
|
||||
awsSpanMetricsProcessor.onStart(parentContextMock, spanMock);
|
||||
verifyNoInteractions(parentContextMock, spanMock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTearDown() {
|
||||
assertThat(awsSpanMetricsProcessor.shutdown()).isEqualTo(CompletableResultCode.ofSuccess());
|
||||
assertThat(awsSpanMetricsProcessor.forceFlush()).isEqualTo(CompletableResultCode.ofSuccess());
|
||||
|
||||
// Not really much to test, just check that it doesn't cause issues/throw anything.
|
||||
awsSpanMetricsProcessor.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests starting with testOnEndMetricsGeneration are testing the logic in
|
||||
* AwsSpanMetricsProcessor's onEnd method pertaining to metrics generation.
|
||||
*/
|
||||
@Test
|
||||
public void testOnEndMetricsGenerationWithoutSpanAttributes() {
|
||||
Attributes spanAttributes = buildSpanAttributes(CONTAINS_NO_ATTRIBUTES);
|
||||
ReadableSpan readableSpanMock = buildReadableSpanMock(spanAttributes);
|
||||
Attributes metricAttributes = buildMetricAttributes(CONTAINS_ATTRIBUTES);
|
||||
configureMocksForOnEnd(readableSpanMock, metricAttributes);
|
||||
|
||||
awsSpanMetricsProcessor.onEnd(readableSpanMock);
|
||||
verifyNoInteractions(errorCounterMock);
|
||||
verifyNoInteractions(faultCounterMock);
|
||||
verify(latencyHistogramMock, times(1)).record(eq(TEST_LATENCY_MILLIS), eq(metricAttributes));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnEndMetricsGenerationWithoutMetricAttributes() {
|
||||
Attributes spanAttributes = Attributes.of(HTTP_STATUS_CODE, 500L);
|
||||
ReadableSpan readableSpanMock = buildReadableSpanMock(spanAttributes);
|
||||
Attributes metricAttributes = buildMetricAttributes(CONTAINS_NO_ATTRIBUTES);
|
||||
configureMocksForOnEnd(readableSpanMock, metricAttributes);
|
||||
|
||||
awsSpanMetricsProcessor.onEnd(readableSpanMock);
|
||||
verifyNoInteractions(errorCounterMock);
|
||||
verifyNoInteractions(faultCounterMock);
|
||||
verifyNoInteractions(latencyHistogramMock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnEndMetricsGenerationWithoutEndRequired() {
|
||||
Attributes spanAttributes = Attributes.of(HTTP_STATUS_CODE, 500L);
|
||||
ReadableSpan readableSpanMock = buildReadableSpanMock(spanAttributes);
|
||||
Attributes metricAttributes = buildMetricAttributes(CONTAINS_ATTRIBUTES);
|
||||
configureMocksForOnEnd(readableSpanMock, metricAttributes);
|
||||
|
||||
awsSpanMetricsProcessor.onEnd(readableSpanMock);
|
||||
verifyNoInteractions(errorCounterMock);
|
||||
verify(faultCounterMock, times(1)).add(eq(1L), eq(metricAttributes));
|
||||
verify(latencyHistogramMock, times(1)).record(eq(TEST_LATENCY_MILLIS), eq(metricAttributes));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnEndMetricsGenerationWithLatency() {
|
||||
Attributes spanAttributes = Attributes.of(HTTP_STATUS_CODE, 200L);
|
||||
ReadableSpan readableSpanMock = buildReadableSpanMock(spanAttributes);
|
||||
Attributes metricAttributes = buildMetricAttributes(CONTAINS_ATTRIBUTES);
|
||||
configureMocksForOnEnd(readableSpanMock, metricAttributes);
|
||||
|
||||
when(readableSpanMock.getLatencyNanos()).thenReturn(5_500_000L);
|
||||
|
||||
awsSpanMetricsProcessor.onEnd(readableSpanMock);
|
||||
verifyNoInteractions(errorCounterMock);
|
||||
verifyNoInteractions(faultCounterMock);
|
||||
verify(latencyHistogramMock, times(1)).record(eq(5.5), eq(metricAttributes));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnEndMetricsGenerationWithStatusCodes() {
|
||||
// Invalid HTTP status codes
|
||||
validateMetricsGeneratedForHttpStatusCode(null, ExpectedStatusMetric.NEITHER);
|
||||
|
||||
// Valid HTTP status codes
|
||||
validateMetricsGeneratedForHttpStatusCode(200L, ExpectedStatusMetric.NEITHER);
|
||||
validateMetricsGeneratedForHttpStatusCode(399L, ExpectedStatusMetric.NEITHER);
|
||||
validateMetricsGeneratedForHttpStatusCode(400L, ExpectedStatusMetric.ERROR);
|
||||
validateMetricsGeneratedForHttpStatusCode(499L, ExpectedStatusMetric.ERROR);
|
||||
validateMetricsGeneratedForHttpStatusCode(500L, ExpectedStatusMetric.FAULT);
|
||||
validateMetricsGeneratedForHttpStatusCode(599L, ExpectedStatusMetric.FAULT);
|
||||
validateMetricsGeneratedForHttpStatusCode(600L, ExpectedStatusMetric.NEITHER);
|
||||
}
|
||||
|
||||
private static Attributes buildSpanAttributes(boolean containsAttribute) {
|
||||
if (containsAttribute) {
|
||||
return Attributes.of(AttributeKey.stringKey("original key"), "original value");
|
||||
} else {
|
||||
return Attributes.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static Attributes buildMetricAttributes(boolean containsAttribute) {
|
||||
if (containsAttribute) {
|
||||
return Attributes.of(AttributeKey.stringKey("new key"), "new value");
|
||||
} else {
|
||||
return Attributes.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static ReadableSpan buildReadableSpanMock(Attributes spanAttributes) {
|
||||
ReadableSpan readableSpanMock = mock(ReadableSpan.class);
|
||||
|
||||
// Configure latency
|
||||
when(readableSpanMock.getLatencyNanos()).thenReturn(TEST_LATENCY_NANOS);
|
||||
|
||||
// Configure attributes
|
||||
when(readableSpanMock.getAttribute(any()))
|
||||
.thenAnswer(invocation -> spanAttributes.get(invocation.getArgument(0)));
|
||||
|
||||
// Configure spanData
|
||||
SpanData mockSpanData = mock(SpanData.class);
|
||||
when(mockSpanData.getAttributes()).thenReturn(spanAttributes);
|
||||
when(mockSpanData.getTotalAttributeCount()).thenReturn(spanAttributes.size());
|
||||
when(readableSpanMock.toSpanData()).thenReturn(mockSpanData);
|
||||
|
||||
return readableSpanMock;
|
||||
}
|
||||
|
||||
private void configureMocksForOnEnd(ReadableSpan readableSpanMock, Attributes metricAttributes) {
|
||||
// Configure generated attributes
|
||||
when(generatorMock.generateMetricAttributesFromSpan(
|
||||
eq(readableSpanMock.toSpanData()), eq(testResource)))
|
||||
.thenReturn(metricAttributes);
|
||||
}
|
||||
|
||||
private void validateMetricsGeneratedForHttpStatusCode(
|
||||
Long httpStatusCode, ExpectedStatusMetric expectedStatusMetric) {
|
||||
Attributes spanAttributes = Attributes.of(HTTP_STATUS_CODE, httpStatusCode);
|
||||
ReadableSpan readableSpanMock = buildReadableSpanMock(spanAttributes);
|
||||
Attributes metricAttributes = buildMetricAttributes(CONTAINS_ATTRIBUTES);
|
||||
configureMocksForOnEnd(readableSpanMock, metricAttributes);
|
||||
|
||||
awsSpanMetricsProcessor.onEnd(readableSpanMock);
|
||||
switch (expectedStatusMetric) {
|
||||
case ERROR:
|
||||
verify(errorCounterMock, times(1)).add(eq(1L), eq(metricAttributes));
|
||||
verifyNoInteractions(faultCounterMock);
|
||||
break;
|
||||
case FAULT:
|
||||
verifyNoInteractions(errorCounterMock);
|
||||
verify(faultCounterMock, times(1)).add(eq(1L), eq(metricAttributes));
|
||||
break;
|
||||
case NEITHER:
|
||||
verifyNoInteractions(errorCounterMock);
|
||||
verifyNoInteractions(faultCounterMock);
|
||||
break;
|
||||
}
|
||||
|
||||
verify(latencyHistogramMock, times(1)).record(eq(TEST_LATENCY_MILLIS), eq(metricAttributes));
|
||||
|
||||
// Clear invocations so this method can be called multiple times in one test.
|
||||
clearInvocations(errorCounterMock);
|
||||
clearInvocations(faultCounterMock);
|
||||
clearInvocations(latencyHistogramMock);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue