
io.opentelemetry.sdk.testing.assertj.SpanDataAssert Maven / Gradle / Ivy
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.testing.assertj;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.api.trace.SpanId;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.TraceState;
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 java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import org.assertj.core.api.AbstractAssert;
/** Assertions for an exported {@link SpanData}. */
public final class SpanDataAssert extends AbstractAssert {
private static final AttributeKey EXCEPTION_TYPE =
AttributeKey.stringKey("exception.type");
private static final AttributeKey EXCEPTION_MESSAGE =
AttributeKey.stringKey("exception.message");
private static final AttributeKey EXCEPTION_STACKTRACE =
AttributeKey.stringKey("exception.stacktrace");
private static final String EXCEPTION_EVENT_NAME = "exception";
SpanDataAssert(@Nullable SpanData actual) {
super(actual, SpanDataAssert.class);
}
/** Asserts the span has the given trace ID. */
public SpanDataAssert hasTraceId(String traceId) {
isNotNull();
if (!actual.getTraceId().equals(traceId)) {
failWithActualExpectedAndMessage(
actual.getTraceId(),
traceId,
"Expected span [%s] to have trace ID <%s> but was <%s>",
actual.getName(),
traceId,
actual.getTraceId());
}
return this;
}
/** Asserts the span has the given span ID. */
public SpanDataAssert hasSpanId(String spanId) {
isNotNull();
if (!actual.getSpanId().equals(spanId)) {
failWithActualExpectedAndMessage(
actual.getSpanId(),
spanId,
"Expected span [%s] to have span ID <%s> but was <%s>",
actual.getName(),
spanId,
actual.getSpanId());
}
return this;
}
/** Asserts the span is sampled. */
public SpanDataAssert isSampled() {
isNotNull();
if (!actual.getSpanContext().isSampled()) {
failWithMessage("Expected span [%s] to be sampled but was not.", actual.getName());
}
return this;
}
/** Asserts the span is not sampled. */
public SpanDataAssert isNotSampled() {
isNotNull();
if (actual.getSpanContext().isSampled()) {
failWithMessage("Expected span [%s] to not be sampled but it was.", actual.getName());
}
return this;
}
/** Asserts the span has the given {@link TraceState}. */
public SpanDataAssert hasTraceState(TraceState traceState) {
isNotNull();
if (!actual.getSpanContext().getTraceState().equals(traceState)) {
failWithActualExpectedAndMessage(
actual.getSpanContext().getTraceState(),
traceState,
"Expected span [%s] to have trace state <%s> but was <%s>",
actual.getName(),
traceState,
actual.getSpanContext().getTraceState());
}
return this;
}
/** Asserts the span has the given parent span ID. */
public SpanDataAssert hasParentSpanId(String parentSpanId) {
isNotNull();
String actualParentSpanId = actual.getParentSpanId();
if (!actualParentSpanId.equals(parentSpanId)) {
failWithActualExpectedAndMessage(
actualParentSpanId,
parentSpanId,
"Expected span [%s] to have parent span ID <%s> but was <%s>",
actual.getName(),
parentSpanId,
actualParentSpanId);
}
return this;
}
/**
* Asserts the span has the given parent {@link SpanData span}.
*
* Equivalent to {@code span.hasParentSpanId(parent.getSpanId())}.
*/
public SpanDataAssert hasParent(SpanData parent) {
return hasParentSpanId(parent.getSpanId());
}
/**
* Asserts the span has no parent {@link SpanData span}.
*
*
Equivalent to {@code span.hasParentSpanId(SpanId.getInvalid())}.
*/
public SpanDataAssert hasNoParent() {
isNotNull();
String actualParentSpanId = actual.getParentSpanId();
if (!actualParentSpanId.equals(SpanId.getInvalid())) {
failWithActualExpectedAndMessage(
actualParentSpanId,
SpanId.getInvalid(),
"Expected span [%s] to have no parent but had parent span ID <%s>",
actual.getName(),
actualParentSpanId);
}
return this;
}
/** Asserts the span has the given {@link Resource}. */
public SpanDataAssert hasResource(Resource resource) {
isNotNull();
if (!actual.getResource().equals(resource)) {
failWithActualExpectedAndMessage(
actual.getResource(),
resource,
"Expected span [%s] to have resource <%s> but was <%s>",
actual.getName(),
resource,
actual.getResource());
}
return this;
}
/**
* Asserts the span has a resource satisfying the given condition.
*
* @since 1.23.0
*/
public SpanDataAssert hasResourceSatisfying(Consumer resource) {
isNotNull();
resource.accept(
new ResourceAssert(actual.getResource(), String.format("span [%s]", actual.getName())));
return this;
}
/**
* Asserts the span has the given {@link io.opentelemetry.sdk.common.InstrumentationLibraryInfo}.
*
* @deprecated Use {@link #hasInstrumentationScopeInfo(InstrumentationScopeInfo)}.
*/
@Deprecated
public SpanDataAssert hasInstrumentationLibraryInfo(
io.opentelemetry.sdk.common.InstrumentationLibraryInfo instrumentationLibraryInfo) {
isNotNull();
if (!actual.getInstrumentationLibraryInfo().equals(instrumentationLibraryInfo)) {
failWithActualExpectedAndMessage(
actual.getInstrumentationLibraryInfo(),
instrumentationLibraryInfo,
"Expected span [%s] to have instrumentation library info <%s> but was <%s>",
actual.getName(),
instrumentationLibraryInfo,
actual.getInstrumentationLibraryInfo());
}
return this;
}
/** Asserts the span has the given {@link InstrumentationScopeInfo}. */
public SpanDataAssert hasInstrumentationScopeInfo(
InstrumentationScopeInfo instrumentationScopeInfo) {
isNotNull();
if (!actual.getInstrumentationScopeInfo().equals(instrumentationScopeInfo)) {
failWithActualExpectedAndMessage(
actual.getInstrumentationScopeInfo(),
instrumentationScopeInfo,
"Expected span [%s] to have instrumentation scope info <%s> but was <%s>",
actual.getName(),
instrumentationScopeInfo,
actual.getInstrumentationScopeInfo());
}
return this;
}
/** Asserts the span has the given name. */
public SpanDataAssert hasName(String name) {
isNotNull();
if (!actual.getName().equals(name)) {
failWithActualExpectedAndMessage(
actual.getName(),
name,
"Expected span to have name <%s> but was <%s>",
name,
actual.getName());
}
return this;
}
/** Asserts the span has the given kind. */
public SpanDataAssert hasKind(SpanKind kind) {
isNotNull();
if (!actual.getKind().equals(kind)) {
failWithActualExpectedAndMessage(
actual.getKind(),
kind,
"Expected span [%s] to have kind <%s> but was <%s>",
actual.getName(),
kind,
actual.getKind());
}
return this;
}
/** Asserts the span starts at the given epoch timestamp, in nanos. */
public SpanDataAssert startsAt(long startEpochNanos) {
isNotNull();
if (actual.getStartEpochNanos() != startEpochNanos) {
failWithActualExpectedAndMessage(
actual.getStartEpochNanos(),
startEpochNanos,
"Expected span [%s] to have start epoch <%s> nanos but was <%s>",
actual.getName(),
startEpochNanos,
actual.getStartEpochNanos());
}
return this;
}
/** Asserts the span starts at the given epoch timestamp. */
@SuppressWarnings("PreferJavaTimeOverload")
public SpanDataAssert startsAt(long startEpoch, TimeUnit unit) {
return startsAt(unit.toNanos(startEpoch));
}
/** Asserts the span starts at the given epoch timestamp. */
public SpanDataAssert startsAt(Instant timestamp) {
return startsAt(toNanos(timestamp));
}
/** Asserts the span has the given attribute. */
public SpanDataAssert hasAttribute(AttributeKey key, T value) {
return hasAttribute(OpenTelemetryAssertions.equalTo(key, value));
}
/** Asserts the span has an attribute matching the {@code attributeAssertion}. */
public SpanDataAssert hasAttribute(AttributeAssertion attributeAssertion) {
isNotNull();
Set> actualKeys = actual.getAttributes().asMap().keySet();
AttributeKey> key = attributeAssertion.getKey();
assertThat(actualKeys).as("span [%s] attribute keys", actual.getName()).contains(key);
Object value = actual.getAttributes().get(key);
AbstractAssert, ?> assertion = AttributeAssertion.attributeValueAssertion(key, value);
attributeAssertion.getAssertion().accept(assertion);
return this;
}
/** Asserts the span has the given attributes. */
public SpanDataAssert hasAttributes(Attributes attributes) {
isNotNull();
if (!AssertUtil.attributesAreEqual(actual.getAttributes(), attributes)) {
failWithActualExpectedAndMessage(
actual.getAttributes(),
attributes,
"Expected span [%s] to have attributes <%s> but was <%s>",
actual.getName(),
attributes,
actual.getAttributes());
}
return this;
}
/** Asserts the span has the given attributes. */
@SuppressWarnings({"rawtypes", "unchecked"})
@SafeVarargs
public final SpanDataAssert hasAttributes(Map.Entry extends AttributeKey>, ?>... entries) {
AttributesBuilder attributesBuilder = Attributes.builder();
for (Map.Entry extends AttributeKey>, ?> attr : entries) {
attributesBuilder.put((AttributeKey) attr.getKey(), attr.getValue());
}
Attributes attributes = attributesBuilder.build();
return hasAttributes(attributes);
}
/** Asserts the span has attributes satisfying the given condition. */
public SpanDataAssert hasAttributesSatisfying(Consumer attributes) {
isNotNull();
assertThat(actual.getAttributes()).as("attributes").satisfies(attributes);
return this;
}
/**
* Asserts the event has attributes matching all {@code assertions}. Assertions can be created
* using methods like {@link OpenTelemetryAssertions#satisfies(AttributeKey,
* OpenTelemetryAssertions.LongAssertConsumer)}.
*
* @since 1.21.0
*/
public SpanDataAssert hasAttributesSatisfying(AttributeAssertion... assertions) {
return hasAttributesSatisfying(Arrays.asList(assertions));
}
/**
* Asserts the event has attributes matching all {@code assertions}. Assertions can be created
* using methods like {@link OpenTelemetryAssertions#satisfies(AttributeKey,
* OpenTelemetryAssertions.LongAssertConsumer)}.
*
* @since 1.21.0
*/
public SpanDataAssert hasAttributesSatisfying(Iterable assertions) {
AssertUtil.assertAttributes(
actual.getAttributes(),
assertions,
String.format("span [%s] attribute keys", actual.getName()));
return this;
}
/**
* Asserts the span has attributes matching all {@code assertions} and no more. Assertions can be
* created using methods like {@link OpenTelemetryAssertions#satisfies(AttributeKey,
* OpenTelemetryAssertions.LongAssertConsumer)}.
*/
public SpanDataAssert hasAttributesSatisfyingExactly(AttributeAssertion... assertions) {
return hasAttributesSatisfyingExactly(Arrays.asList(assertions));
}
/**
* Asserts the span has attributes matching all {@code assertions} and no more. Assertions can be
* created using methods like {@link OpenTelemetryAssertions#satisfies(AttributeKey,
* OpenTelemetryAssertions.LongAssertConsumer)}.
*/
public SpanDataAssert hasAttributesSatisfyingExactly(Iterable assertions) {
AssertUtil.assertAttributesExactly(
actual.getAttributes(),
assertions,
String.format("span [%s] attribute keys", actual.getName()));
return this;
}
/**
* Asserts the span has an exception event for the given {@link Throwable}. The stack trace is not
* matched against.
*/
// Workaround "passing @Nullable parameter 'stackTrace' where @NonNull is required", Nullaway
// seems to think assertThat is supposed to be passed NonNull even though we know that can't be
// true for assertions.
@SuppressWarnings("NullAway")
public SpanDataAssert hasException(Throwable exception) {
EventData exceptionEvent =
actual.getEvents().stream()
.filter(event -> event.getName().equals(EXCEPTION_EVENT_NAME))
.findFirst()
.orElse(null);
if (exceptionEvent == null) {
failWithMessage(
"Expected span [%s] to have an exception event but only had events <%s>",
actual.getName(), actual.getEvents());
// Never executed but to reduce IntelliJ warnings.
return this;
}
assertThat(exceptionEvent.getAttributes())
.as("exception.type")
.containsEntry(EXCEPTION_TYPE, exception.getClass().getCanonicalName());
if (exception.getMessage() != null) {
assertThat(exceptionEvent.getAttributes())
.as("exception.message")
.containsEntry(EXCEPTION_MESSAGE, exception.getMessage());
}
// Exceptions used in assertions always have a different stack trace, just confirm it was
// recorded.
String stackTrace = exceptionEvent.getAttributes().get(EXCEPTION_STACKTRACE);
assertThat(stackTrace).as("exception.stacktrace").isNotNull();
return this;
}
/** Asserts the span has the given events. */
public SpanDataAssert hasEvents(Iterable events) {
isNotNull();
assertThat(actual.getEvents())
.withFailMessage(
"Expected span [%s] to have events <%s> but was <%s>",
actual.getName(), events, actual.getEvents())
.containsExactlyInAnyOrderElementsOf(events);
return this;
}
/** Asserts the span has the given events. */
public SpanDataAssert hasEvents(EventData... events) {
return hasEvents(Arrays.asList(events));
}
/** Asserts the span has events satisfying the given condition. */
public SpanDataAssert hasEventsSatisfying(Consumer> condition) {
isNotNull();
assertThat(actual.getEvents()).satisfies(condition);
return this;
}
/**
* Asserts that the span under assertion has the same number of events as provided {@code
* assertions} and executes each {@link EventDataAssert} in {@code assertions} in order with the
* corresponding event.
*/
@SafeVarargs
@SuppressWarnings("varargs")
public final SpanDataAssert hasEventsSatisfyingExactly(Consumer... assertions) {
assertThat(actual.getEvents()).hasSize(assertions.length);
// Avoid zipSatisfy - https://github.com/assertj/assertj-core/issues/2300
for (int i = 0; i < assertions.length; i++) {
assertions[i].accept(new EventDataAssert(actual.getEvents().get(i)));
}
return this;
}
/** Asserts the span has the given links. */
public SpanDataAssert hasLinks(Iterable links) {
isNotNull();
assertThat(actual.getLinks())
.withFailMessage(
"Expected span [%s] to have links <%s> but was <%s>",
actual.getName(), links, actual.getLinks())
.containsExactlyInAnyOrderElementsOf(links);
return this;
}
/** Asserts the span has the given links. */
public SpanDataAssert hasLinks(LinkData... links) {
return hasLinks(Arrays.asList(links));
}
/** Asserts the span has events satisfying the given condition. */
public SpanDataAssert hasLinksSatisfying(Consumer> condition) {
isNotNull();
assertThat(actual.getLinks()).satisfies(condition);
return this;
}
/**
* Asserts the span has the given {@link StatusData}.
*
* @since 1.16.0
*/
public SpanDataAssert hasStatus(StatusData status) {
isNotNull();
if (!actual.getStatus().equals(status)) {
failWithActualExpectedAndMessage(
actual.getStatus(),
status,
"Expected span [%s] to have status <%s> but was <%s>",
actual.getName(),
status,
actual.getStatus());
}
return this;
}
/** Asserts the span has a status satisfying the given condition. */
public SpanDataAssert hasStatusSatisfying(Consumer condition) {
isNotNull();
StatusDataAssert statusDataAssert = new StatusDataAssert(actual.getStatus());
condition.accept(statusDataAssert);
return this;
}
/** Asserts the span ends at the given epoch timestamp, in nanos. */
public SpanDataAssert endsAt(long endEpochNanos) {
isNotNull();
if (actual.getEndEpochNanos() != endEpochNanos) {
failWithActualExpectedAndMessage(
actual.getEndEpochNanos(),
endEpochNanos,
"Expected span [%s] to have end epoch <%s> nanos but was <%s>",
actual.getName(),
endEpochNanos,
actual.getEndEpochNanos());
}
return this;
}
/** Asserts the span ends at the given epoch timestamp. */
@SuppressWarnings("PreferJavaTimeOverload")
public SpanDataAssert endsAt(long startEpoch, TimeUnit unit) {
return endsAt(unit.toNanos(startEpoch));
}
/** Asserts the span ends at the given epoch timestamp. */
public SpanDataAssert endsAt(Instant timestamp) {
return endsAt(toNanos(timestamp));
}
/** Asserts the span has ended. */
public SpanDataAssert hasEnded() {
isNotNull();
if (!actual.hasEnded()) {
failWithMessage("Expected span [%s] to have ended but did not", actual.getName());
}
return this;
}
/** Asserts the span has not ended. */
public SpanDataAssert hasNotEnded() {
isNotNull();
if (actual.hasEnded()) {
failWithMessage("Expected span [%s] to have not ended but did has", actual.getName());
}
return this;
}
/** Asserts the span has the given total recorded events. */
public SpanDataAssert hasTotalRecordedEvents(int totalRecordedEvents) {
isNotNull();
if (actual.getTotalRecordedEvents() != totalRecordedEvents) {
failWithActualExpectedAndMessage(
actual.getTotalRecordedEvents(),
totalRecordedEvents,
"Expected span [%s] to have recorded <%s> total events but did not",
actual.getName(),
totalRecordedEvents,
actual.getTotalRecordedEvents());
}
return this;
}
/** Asserts the span has the given total recorded links. */
public SpanDataAssert hasTotalRecordedLinks(int totalRecordedLinks) {
isNotNull();
if (actual.getTotalRecordedLinks() != totalRecordedLinks) {
failWithActualExpectedAndMessage(
actual.getTotalRecordedLinks(),
totalRecordedLinks,
"Expected span [%s] to have recorded <%s> total links but did not",
actual.getName(),
totalRecordedLinks,
actual.getTotalRecordedLinks());
}
return this;
}
/** Asserts the span has the given total attributes. */
public SpanDataAssert hasTotalAttributeCount(int totalAttributeCount) {
isNotNull();
if (actual.getTotalAttributeCount() != totalAttributeCount) {
failWithActualExpectedAndMessage(
actual.getTotalAttributeCount(),
totalAttributeCount,
"Expected span [%s] to have recorded <%s> total attributes but did not",
actual.getName(),
totalAttributeCount,
actual.getTotalAttributeCount());
}
return this;
}
private static long toNanos(Instant timestamp) {
return TimeUnit.SECONDS.toNanos(timestamp.getEpochSecond()) + timestamp.getNano();
}
}