Add new components to allow for generating metrics from 100% of spans without impacting sampling (#802)

This commit is contained in:
Thomas Pierce 2023-05-15 16:05:50 -07:00 committed by GitHub
parent d305bf68c9
commit fbf8304688
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1703 additions and 0 deletions

View File

@ -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);
}
};
}
}

View File

@ -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");

View File

@ -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);
}
}

View File

@ -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;
}
};
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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();
}
};
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}