Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ExceptionEventData which wraps events that contain exceptions #4162

Merged
merged 10 commits into from
Mar 31, 2022
7 changes: 1 addition & 6 deletions docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,2 @@
Comparing source compatibility of against
+++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.sdk.trace.data.ExceptionEventData (not serializable)
+++ CLASS FILE FORMAT VERSION: 52.0 <- n.a.
+++ NEW INTERFACE: io.opentelemetry.sdk.trace.data.EventData
+++ NEW SUPERCLASS: java.lang.Object
+++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.sdk.trace.data.ExceptionEventData create(long, java.lang.Throwable, io.opentelemetry.api.common.Attributes)
+++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.Throwable getException()
No changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.sdk.trace.data;
package io.opentelemetry.sdk.extension.incubator.trace.data;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.sdk.trace.data.EventData;

public interface ExceptionEventData extends EventData {

Expand All @@ -28,4 +29,11 @@ static ExceptionEventData create(
* @return the {@link Throwable exception} of the {@link ExceptionEventData}
*/
Throwable getException();

/**
* Return the additional {@link Attributes attributes} of the {@link ExceptionEventData}.
*
* @return the additional {@link Attributes attributes} of the {@link ExceptionEventData}
*/
Attributes getAdditionalAttributes();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.sdk.extension.incubator.trace.data;

import com.google.auto.value.AutoValue;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.io.PrintWriter;
import java.io.StringWriter;
import javax.annotation.concurrent.Immutable;

/** An effectively immutable implementation of {@link ExceptionEventData}. */
@AutoValue
@Immutable
abstract class ImmutableExceptionEventData implements ExceptionEventData {

/**
* Returns a new immutable {@code Event}.
*
* @param epochNanos epoch timestamp in nanos of the {@code Event}.
* @param exception the {@link Throwable exception} of the {@code Event}.
* @param additionalAttributes the additional {@link Attributes} of the {@code Event}.
* @return a new immutable {@code Event<T>}
*/
static ExceptionEventData create(
long epochNanos, Throwable exception, Attributes additionalAttributes) {

AttributesBuilder attributesBuilder = Attributes.builder();
attributesBuilder.put(
SemanticAttributes.EXCEPTION_TYPE, exception.getClass().getCanonicalName());
String message = exception.getMessage();
if (message != null) {
attributesBuilder.put(SemanticAttributes.EXCEPTION_MESSAGE, message);
}

StringWriter stringWriter = new StringWriter();
try (PrintWriter printWriter = new PrintWriter(stringWriter)) {
exception.printStackTrace(printWriter);
}
attributesBuilder.put(SemanticAttributes.EXCEPTION_STACKTRACE, stringWriter.toString());

if (additionalAttributes != null) {
attributesBuilder.putAll(additionalAttributes);
}
Attributes attributes = attributesBuilder.build();

return new AutoValue_ImmutableExceptionEventData(
SemanticAttributes.EXCEPTION_EVENT_NAME,
attributes,
epochNanos,
attributes.size(),
exception,
additionalAttributes);
}

ImmutableExceptionEventData() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.sdk.extension.incubator.trace;

import static io.opentelemetry.api.common.AttributeKey.stringKey;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.extension.incubator.trace.data.ExceptionEventData;
import io.opentelemetry.sdk.testing.time.TestClock;
import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.SpanProcessor;
import io.opentelemetry.sdk.trace.data.EventData;
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Duration;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class ExceptionEventDataTest {

@Mock private SpanProcessor spanProcessor;
@Captor private ArgumentCaptor<ReadableSpan> spanArgumentCaptor;

private TestClock testClock;
private OpenTelemetry openTelemetry;

@BeforeEach
void setUp() {

testClock = TestClock.create();
openTelemetry =
OpenTelemetrySdk.builder()
.setTracerProvider(
SdkTracerProvider.builder()
.addSpanProcessor(spanProcessor)
.setClock(testClock)
.build())
.build();
}

@Test
void recordException() {
Exception exception = new IllegalStateException("there was an exception");
String stackTrace = stackTrace(exception);

testClock.advance(Duration.ofNanos(1000));
long timestamp = testClock.now();

Span span = createSpan();
span.recordException(exception);
span.end();

verify(spanProcessor).onEnd(spanArgumentCaptor.capture());
ReadableSpan capturedSpan = spanArgumentCaptor.getValue();
SpanData spanData = capturedSpan.toSpanData();
List<EventData> events = spanData.getEvents();
assertThat(events).hasSize(1);

EventData event = events.get(0);
assertThat(event.getName()).isEqualTo(SemanticAttributes.EXCEPTION_EVENT_NAME);
assertThat(event.getEpochNanos()).isEqualTo(timestamp);
assertThat(event.getAttributes())
.isEqualTo(
Attributes.of(
SemanticAttributes.EXCEPTION_TYPE, "java.lang.IllegalStateException",
SemanticAttributes.EXCEPTION_MESSAGE, "there was an exception",
SemanticAttributes.EXCEPTION_STACKTRACE, stackTrace));
assertThat(event.getTotalAttributeCount()).isEqualTo(3);

assertThat(event).isInstanceOf(ExceptionEventData.class);
ExceptionEventData exceptionEvent = (ExceptionEventData) event;
assertThat(exceptionEvent.getException()).isSameAs(exception);
assertThat(exceptionEvent.getAdditionalAttributes()).isEqualTo(Attributes.empty());
}

@Test
void recordException_noMessage() {
Exception exception = new IllegalStateException();
String stackTrace = stackTrace(exception);

testClock.advance(Duration.ofNanos(1000));
long timestamp = testClock.now();

Span span = createSpan();
span.recordException(exception);
span.end();

verify(spanProcessor).onEnd(spanArgumentCaptor.capture());
ReadableSpan capturedSpan = spanArgumentCaptor.getValue();
SpanData spanData = capturedSpan.toSpanData();
List<EventData> events = spanData.getEvents();
assertThat(events).hasSize(1);

EventData event = events.get(0);
assertThat(event.getName()).isEqualTo(SemanticAttributes.EXCEPTION_EVENT_NAME);
assertThat(event.getEpochNanos()).isEqualTo(timestamp);
assertThat(event.getAttributes())
.isEqualTo(
Attributes.of(
SemanticAttributes.EXCEPTION_TYPE,
"java.lang.IllegalStateException",
SemanticAttributes.EXCEPTION_STACKTRACE,
stackTrace));
assertThat(event.getTotalAttributeCount()).isEqualTo(2);

assertThat(event).isInstanceOf(ExceptionEventData.class);
ExceptionEventData exceptionEvent = (ExceptionEventData) event;
assertThat(exceptionEvent.getException()).isSameAs(exception);
assertThat(exceptionEvent.getAdditionalAttributes()).isEqualTo(Attributes.empty());
}

@Test
void recordException_innerClassException() {
Exception exception = new InnerClassException("there was an exception");
String stackTrace = stackTrace(exception);

testClock.advance(Duration.ofNanos(1000));
long timestamp = testClock.now();

Span span = createSpan();
span.recordException(exception);
span.end();

verify(spanProcessor).onEnd(spanArgumentCaptor.capture());
ReadableSpan capturedSpan = spanArgumentCaptor.getValue();
SpanData spanData = capturedSpan.toSpanData();
List<EventData> events = spanData.getEvents();
assertThat(events).hasSize(1);

EventData event = events.get(0);
assertThat(event.getName()).isEqualTo(SemanticAttributes.EXCEPTION_EVENT_NAME);
assertThat(event.getEpochNanos()).isEqualTo(timestamp);
assertThat(event.getAttributes())
.isEqualTo(
Attributes.of(
SemanticAttributes.EXCEPTION_TYPE,
"io.opentelemetry.sdk.extension.incubator.trace.ExceptionEventDataTest.InnerClassException",
SemanticAttributes.EXCEPTION_MESSAGE, "there was an exception",
SemanticAttributes.EXCEPTION_STACKTRACE, stackTrace));
assertThat(event.getTotalAttributeCount()).isEqualTo(3);

assertThat(event).isInstanceOf(ExceptionEventData.class);
ExceptionEventData exceptionEvent = (ExceptionEventData) event;
assertThat(exceptionEvent.getException()).isSameAs(exception);
assertThat(exceptionEvent.getAdditionalAttributes()).isEqualTo(Attributes.empty());
}

@Test
void recordException_additionalAttributes() {
Exception exception = new IllegalStateException("there was an exception");
String stackTrace = stackTrace(exception);

testClock.advance(Duration.ofNanos(1000));
long timestamp = testClock.now();

Span span = createSpan();
span.recordException(
exception,
Attributes.of(
stringKey("key1"),
"this is an additional attribute",
stringKey("exception.message"),
"this is a precedence attribute"));
span.end();

verify(spanProcessor).onEnd(spanArgumentCaptor.capture());
ReadableSpan capturedSpan = spanArgumentCaptor.getValue();
SpanData spanData = capturedSpan.toSpanData();
List<EventData> events = spanData.getEvents();
assertThat(events).hasSize(1);

EventData event = events.get(0);
assertThat(event.getName()).isEqualTo(SemanticAttributes.EXCEPTION_EVENT_NAME);
assertThat(event.getEpochNanos()).isEqualTo(timestamp);
assertThat(event.getAttributes())
.isEqualTo(
Attributes.builder()
.put(SemanticAttributes.EXCEPTION_TYPE, "java.lang.IllegalStateException")
.put(SemanticAttributes.EXCEPTION_MESSAGE, "this is a precedence attribute")
.put(SemanticAttributes.EXCEPTION_STACKTRACE, stackTrace)
.put("key1", "this is an additional attribute")
.build());
assertThat(event.getTotalAttributeCount()).isEqualTo(4);

assertThat(event).isInstanceOf(ExceptionEventData.class);
ExceptionEventData exceptionEvent = (ExceptionEventData) event;
assertThat(exceptionEvent.getException()).isSameAs(exception);
assertThat(exceptionEvent.getAdditionalAttributes())
.isEqualTo(
Attributes.of(
stringKey("key1"),
"this is an additional attribute",
stringKey("exception.message"),
"this is a precedence attribute"));
}

private Span createSpan() {
return openTelemetry.getTracer("test").spanBuilder("test").startSpan();
}

private static String stackTrace(Throwable exception) {
StringWriter stringWriter = new StringWriter();
try (PrintWriter printWriter = new PrintWriter(stringWriter)) {
exception.printStackTrace(printWriter);
}
return stringWriter.toString();
}

private static class InnerClassException extends Exception {
public InnerClassException(String message) {
super(message);
}
}
}
Loading