diff --git a/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/crash/CrashDetails.java b/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/crash/CrashDetails.java index 220549139..2b41c96c3 100644 --- a/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/crash/CrashDetails.java +++ b/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/crash/CrashDetails.java @@ -31,10 +31,6 @@ public Throwable getCause() { return cause; } - String spanName() { - return getCause().getClass().getSimpleName(); - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/crash/CrashDetailsAttributesExtractor.java b/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/crash/CrashDetailsAttributesExtractor.java deleted file mode 100644 index 1f3091088..000000000 --- a/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/crash/CrashDetailsAttributesExtractor.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.android.instrumentation.crash; - -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.semconv.SemanticAttributes; - -final class CrashDetailsAttributesExtractor implements AttributesExtractor { - - @Override - public void onStart( - AttributesBuilder attributes, Context parentContext, CrashDetails crashDetails) { - attributes.put(SemanticAttributes.THREAD_ID, crashDetails.getThread().getId()); - attributes.put(SemanticAttributes.THREAD_NAME, crashDetails.getThread().getName()); - attributes.put(SemanticAttributes.EXCEPTION_ESCAPED, true); - } - - @Override - public void onEnd( - AttributesBuilder attributes, - Context context, - CrashDetails crashDetails, - Void unused, - Throwable error) {} -} diff --git a/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/crash/CrashReporter.java b/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/crash/CrashReporter.java index bff259b81..7f6d1ce06 100644 --- a/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/crash/CrashReporter.java +++ b/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/crash/CrashReporter.java @@ -6,10 +6,17 @@ package io.opentelemetry.android.instrumentation.crash; import io.opentelemetry.android.instrumentation.InstrumentedApplication; -import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.api.logs.LoggerProvider; +import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.semconv.SemanticAttributes; +import java.io.PrintWriter; +import java.io.StringWriter; import java.util.List; +import java.util.function.Consumer; /** Entrypoint for installing the crash reporting instrumentation. */ public final class CrashReporter { @@ -38,16 +45,45 @@ public void installOn(InstrumentedApplication instrumentedApplication) { Thread.getDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler( new CrashReportingExceptionHandler( - buildInstrumenter(instrumentedApplication.getOpenTelemetrySdk()), - instrumentedApplication.getOpenTelemetrySdk().getSdkTracerProvider(), + buildInstrumenter( + instrumentedApplication + .getOpenTelemetrySdk() + .getSdkLoggerProvider()), + instrumentedApplication.getOpenTelemetrySdk().getSdkLoggerProvider(), existingHandler)); } - private Instrumenter buildInstrumenter(OpenTelemetry openTelemetry) { - return Instrumenter.builder( - openTelemetry, "io.opentelemetry.crash", CrashDetails::spanName) - .addAttributesExtractor(new CrashDetailsAttributesExtractor()) - .addAttributesExtractors(additionalExtractors) - .buildInstrumenter(); + private void emitCrashEvent(Logger crashReporter, CrashDetails crashDetails) { + Throwable throwable = crashDetails.getCause(); + Thread thread = crashDetails.getThread(); + AttributesBuilder attributesBuilder = + Attributes.builder() + .put(SemanticAttributes.EXCEPTION_ESCAPED, true) + .put(SemanticAttributes.THREAD_ID, thread.getId()) + .put(SemanticAttributes.THREAD_NAME, thread.getName()) + .put(SemanticAttributes.EXCEPTION_MESSAGE, throwable.getMessage()) + .put(SemanticAttributes.EXCEPTION_STACKTRACE, stackTraceToString(throwable)) + .put(SemanticAttributes.EXCEPTION_TYPE, throwable.getClass().getName()); + + for (AttributesExtractor extractor : additionalExtractors) { + extractor.onStart(attributesBuilder, Context.current(), crashDetails); + } + + crashReporter.logRecordBuilder().setAllAttributes(attributesBuilder.build()).emit(); + } + + private String stackTraceToString(Throwable throwable) { + StringWriter sw = new StringWriter(256); + PrintWriter pw = new PrintWriter(sw); + + throwable.printStackTrace(pw); + pw.flush(); + + return sw.toString(); + } + + private Consumer buildInstrumenter(LoggerProvider loggerProvider) { + Logger logger = loggerProvider.loggerBuilder("io.opentelemetry.crash").build(); + return crashDetails -> emitCrashEvent(logger, crashDetails); } } diff --git a/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/crash/CrashReportingExceptionHandler.java b/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/crash/CrashReportingExceptionHandler.java index 6c404ead3..4532bd835 100644 --- a/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/crash/CrashReportingExceptionHandler.java +++ b/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/crash/CrashReportingExceptionHandler.java @@ -6,24 +6,23 @@ package io.opentelemetry.android.instrumentation.crash; import androidx.annotation.NonNull; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.sdk.common.CompletableResultCode; -import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; final class CrashReportingExceptionHandler implements Thread.UncaughtExceptionHandler { - private final Instrumenter instrumenter; - private final SdkTracerProvider sdkTracerProvider; + private final Consumer crashSender; + private final SdkLoggerProvider sdkLoggerProvider; private final Thread.UncaughtExceptionHandler existingHandler; CrashReportingExceptionHandler( - Instrumenter instrumenter, - SdkTracerProvider sdkTracerProvider, + Consumer crashSender, + SdkLoggerProvider sdkLoggerProvider, Thread.UncaughtExceptionHandler existingHandler) { - this.instrumenter = instrumenter; - this.sdkTracerProvider = sdkTracerProvider; + this.crashSender = crashSender; + this.sdkLoggerProvider = sdkLoggerProvider; this.existingHandler = existingHandler; } @@ -32,7 +31,7 @@ public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) { reportCrash(t, e); // do our best to make sure the crash makes it out of the VM - CompletableResultCode flushResult = sdkTracerProvider.forceFlush(); + CompletableResultCode flushResult = sdkLoggerProvider.forceFlush(); flushResult.join(10, TimeUnit.SECONDS); // preserve any existing behavior @@ -43,7 +42,6 @@ public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) { private void reportCrash(Thread t, Throwable e) { CrashDetails crashDetails = CrashDetails.create(t, e); - Context context = instrumenter.start(Context.current(), crashDetails); - instrumenter.end(context, crashDetails, null, e); + crashSender.accept(crashDetails); } } diff --git a/instrumentation/src/test/java/io/opentelemetry/android/instrumentation/crash/CrashReporterTest.java b/instrumentation/src/test/java/io/opentelemetry/android/instrumentation/crash/CrashReporterTest.java index 79498adc7..147842477 100644 --- a/instrumentation/src/test/java/io/opentelemetry/android/instrumentation/crash/CrashReporterTest.java +++ b/instrumentation/src/test/java/io/opentelemetry/android/instrumentation/crash/CrashReporterTest.java @@ -7,20 +7,18 @@ import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor.constant; -import static org.awaitility.Awaitility.await; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import io.opentelemetry.android.instrumentation.InstrumentedApplication; import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.testing.assertj.TraceAssert; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; -import io.opentelemetry.sdk.trace.data.StatusData; import io.opentelemetry.semconv.SemanticAttributes; -import java.time.Duration; -import java.util.function.Consumer; +import java.util.List; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -56,7 +54,8 @@ void integrationTest() throws InterruptedException { .build() .installOn(instrumentedApplication); - RuntimeException crash = new RuntimeException("boooom!"); + String exceptionMessage = "boooom!"; + RuntimeException crash = new RuntimeException(exceptionMessage); Thread crashingThread = new Thread( () -> { @@ -66,26 +65,18 @@ void integrationTest() throws InterruptedException { crashingThread.start(); crashingThread.join(); - Attributes expectedAttributes = - Attributes.builder() - .put(SemanticAttributes.EXCEPTION_ESCAPED, true) - .put(SemanticAttributes.THREAD_ID, crashingThread.getId()) - .put(SemanticAttributes.THREAD_NAME, crashingThread.getName()) - .put(stringKey("test.key"), "abc") - .build(); - assertTrace( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("RuntimeException") - .hasKind(SpanKind.INTERNAL) - .hasStatus(StatusData.error()) - .hasException(crash) - .hasAttributes(expectedAttributes))); - } + List logRecords = testing.getLogRecords(); + assertThat(logRecords).hasSize(1); - private static void assertTrace(Consumer assertion) { - await().atMost(Duration.ofSeconds(30)) - .untilAsserted(() -> testing.assertTraces().hasTracesSatisfyingExactly(assertion)); + Attributes crashAttributes = logRecords.get(0).getAttributes(); + OpenTelemetryAssertions.assertThat(crashAttributes) + .containsEntry(SemanticAttributes.EXCEPTION_ESCAPED, true) + .containsEntry(SemanticAttributes.EXCEPTION_MESSAGE, exceptionMessage) + .containsEntry(SemanticAttributes.EXCEPTION_TYPE, "java.lang.RuntimeException") + .containsEntry(SemanticAttributes.THREAD_ID, crashingThread.getId()) + .containsEntry(SemanticAttributes.THREAD_NAME, crashingThread.getName()) + .containsEntry(stringKey("test.key"), "abc"); + assertThat(crashAttributes.get(SemanticAttributes.EXCEPTION_STACKTRACE)) + .startsWith("java.lang.RuntimeException: boooom!"); } } diff --git a/instrumentation/src/test/java/io/opentelemetry/android/instrumentation/crash/CrashReportingExceptionHandlerTest.java b/instrumentation/src/test/java/io/opentelemetry/android/instrumentation/crash/CrashReportingExceptionHandlerTest.java index 072903824..2c02bbf95 100644 --- a/instrumentation/src/test/java/io/opentelemetry/android/instrumentation/crash/CrashReportingExceptionHandlerTest.java +++ b/instrumentation/src/test/java/io/opentelemetry/android/instrumentation/crash/CrashReportingExceptionHandlerTest.java @@ -5,17 +5,13 @@ package io.opentelemetry.android.instrumentation.crash; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.when; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.sdk.common.CompletableResultCode; -import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InOrder; @@ -25,18 +21,17 @@ @ExtendWith(MockitoExtension.class) class CrashReportingExceptionHandlerTest { - @Mock Instrumenter instrumenter; - @Mock SdkTracerProvider sdkTracerProvider; + @Mock Consumer crashSender; + @Mock SdkLoggerProvider sdkLoggerProvider; @Mock Thread.UncaughtExceptionHandler existingHandler; @Mock CompletableResultCode flushResult; @Test void shouldReportCrash() { - when(sdkTracerProvider.forceFlush()).thenReturn(flushResult); + when(sdkLoggerProvider.forceFlush()).thenReturn(flushResult); CrashReportingExceptionHandler handler = - new CrashReportingExceptionHandler( - instrumenter, sdkTracerProvider, existingHandler); + new CrashReportingExceptionHandler(crashSender, sdkLoggerProvider, existingHandler); NullPointerException oopsie = new NullPointerException("oopsie"); Thread crashThread = new Thread("badThread"); @@ -44,10 +39,9 @@ void shouldReportCrash() { handler.uncaughtException(crashThread, oopsie); CrashDetails crashDetails = CrashDetails.create(crashThread, oopsie); - InOrder io = inOrder(instrumenter, sdkTracerProvider, flushResult, existingHandler); - io.verify(instrumenter).start(Context.current(), crashDetails); - io.verify(instrumenter).end(any(), eq(crashDetails), isNull(), eq(oopsie)); - io.verify(sdkTracerProvider).forceFlush(); + InOrder io = inOrder(crashSender, sdkLoggerProvider, flushResult, existingHandler); + io.verify(crashSender).accept(crashDetails); + io.verify(sdkLoggerProvider).forceFlush(); io.verify(flushResult).join(10, TimeUnit.SECONDS); io.verify(existingHandler).uncaughtException(crashThread, oopsie); io.verifyNoMoreInteractions();