From 7f73c988dec720ca52dcc811b67931a5657b533b Mon Sep 17 00:00:00 2001 From: jason plumb <75337021+breedx-splk@users.noreply.github.com> Date: Wed, 5 Oct 2022 08:43:18 -0700 Subject: [PATCH] Add runtime details to crash span (#369) * add runtime details to crash span * make field volatile instead of atomicreference * add nullable annotation * fix tests --- .../java/com/splunk/rum/CrashReporter.java | 43 +++++++----- .../java/com/splunk/rum/RumInitializer.java | 5 +- .../java/com/splunk/rum/RuntimeDetails.java | 65 ++++++++++++++++++ .../main/java/com/splunk/rum/SplunkRum.java | 5 ++ .../com/splunk/rum/CrashReporterTest.java | 26 ++++++-- .../com/splunk/rum/RumInitializerTest.java | 12 ++++ .../com/splunk/rum/RuntimeDetailsTest.java | 66 +++++++++++++++++++ 7 files changed, 201 insertions(+), 21 deletions(-) create mode 100644 splunk-otel-android/src/main/java/com/splunk/rum/RuntimeDetails.java create mode 100644 splunk-otel-android/src/test/java/com/splunk/rum/RuntimeDetailsTest.java diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/CrashReporter.java b/splunk-otel-android/src/main/java/com/splunk/rum/CrashReporter.java index 5e0ec06b6..4dd5796ab 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/CrashReporter.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/CrashReporter.java @@ -17,9 +17,9 @@ package com.splunk.rum; import androidx.annotation.NonNull; +import io.opentelemetry.api.trace.SpanBuilder; import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.api.trace.Tracer; -import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; @@ -28,12 +28,13 @@ class CrashReporter { - static void initializeCrashReporting(Tracer tracer, OpenTelemetrySdk openTelemetrySdk) { + static void initializeCrashReporting( + Tracer tracer, SdkTracerProvider sdkTracerProvider, RuntimeDetails runtimeDetails) { Thread.UncaughtExceptionHandler existingHandler = Thread.getDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler( new CrashReportingExceptionHandler( - tracer, openTelemetrySdk.getSdkTracerProvider(), existingHandler)); + tracer, sdkTracerProvider, existingHandler, runtimeDetails)); } // visible for testing @@ -41,15 +42,18 @@ static class CrashReportingExceptionHandler implements Thread.UncaughtExceptionH private final Tracer tracer; private final Thread.UncaughtExceptionHandler existingHandler; private final SdkTracerProvider sdkTracerProvider; + private final RuntimeDetails runtimeDetails; private final AtomicBoolean crashHappened = new AtomicBoolean(false); CrashReportingExceptionHandler( Tracer tracer, SdkTracerProvider sdkTracerProvider, - Thread.UncaughtExceptionHandler existingHandler) { + Thread.UncaughtExceptionHandler existingHandler, + RuntimeDetails runtimeDetails) { this.tracer = tracer; this.existingHandler = existingHandler; this.sdkTracerProvider = sdkTracerProvider; + this.runtimeDetails = runtimeDetails; } @Override @@ -65,18 +69,27 @@ public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) { : SplunkRum.COMPONENT_ERROR; String exceptionType = e.getClass().getSimpleName(); - tracer.spanBuilder(exceptionType) - .setAttribute(SemanticAttributes.THREAD_ID, t.getId()) - .setAttribute(SemanticAttributes.THREAD_NAME, t.getName()) - .setAttribute(SemanticAttributes.EXCEPTION_ESCAPED, true) - .setAttribute(SplunkRum.COMPONENT_KEY, component) - .startSpan() - .recordException(e) - .setStatus(StatusCode.ERROR) - .end(); + SpanBuilder builder = + tracer.spanBuilder(exceptionType) + .setAttribute(SemanticAttributes.THREAD_ID, t.getId()) + .setAttribute(SemanticAttributes.THREAD_NAME, t.getName()) + .setAttribute(SemanticAttributes.EXCEPTION_ESCAPED, true) + .setAttribute(SplunkRum.COMPONENT_KEY, component) + .setAttribute( + SplunkRum.STORAGE_SPACE_FREE_KEY, + runtimeDetails.getCurrentStorageFreeSpaceInBytes()) + .setAttribute( + SplunkRum.HEAP_FREE_KEY, + runtimeDetails.getCurrentFreeHeapInBytes()); + + Double currentBatteryPercent = runtimeDetails.getCurrentBatteryPercent(); + if (currentBatteryPercent != null) { + builder.setAttribute(SplunkRum.BATTERY_PERCENT_KEY, currentBatteryPercent); + } + builder.startSpan().recordException(e).setStatus(StatusCode.ERROR).end(); // do our best to make sure the crash makes it out of the VM - CompletableResultCode result = sdkTracerProvider.forceFlush(); - result.join(10, TimeUnit.SECONDS); + CompletableResultCode flushResult = sdkTracerProvider.forceFlush(); + flushResult.join(10, TimeUnit.SECONDS); // preserve any existing behavior: if (existingHandler != null) { existingHandler.uncaughtException(t, e); diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java b/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java index a4820880f..cd8dfe4e2 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java @@ -162,7 +162,10 @@ SplunkRum initialize(ConnectionUtil.Factory connectionUtilFactory, Looper mainLo "activityLifecycleCallbacksInitialized", timingClock.now())); if (builder.crashReportingEnabled) { - CrashReporter.initializeCrashReporting(tracer, openTelemetrySdk); + RuntimeDetails runtimeDetails = + RuntimeDetails.create(application.getApplicationContext()); + CrashReporter.initializeCrashReporting( + tracer, openTelemetrySdk.getSdkTracerProvider(), runtimeDetails); initializationEvents.add( new RumInitializer.InitializationEvent( "crashReportingInitialized", timingClock.now())); diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/RuntimeDetails.java b/splunk-otel-android/src/main/java/com/splunk/rum/RuntimeDetails.java new file mode 100644 index 000000000..0fad20123 --- /dev/null +++ b/splunk-otel-android/src/main/java/com/splunk/rum/RuntimeDetails.java @@ -0,0 +1,65 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.rum; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; +import androidx.annotation.Nullable; +import java.io.File; + +/** Represents details about the runtime environment at a time */ +final class RuntimeDetails extends BroadcastReceiver { + + private @Nullable volatile Double batteryPercent = null; + private final File filesDir; + + static RuntimeDetails create(Context context) { + IntentFilter batteryChangedFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + File filesDir = context.getFilesDir(); + RuntimeDetails runtimeDetails = new RuntimeDetails(filesDir); + context.registerReceiver(runtimeDetails, batteryChangedFilter); + return runtimeDetails; + } + + private RuntimeDetails(File filesDir) { + this.filesDir = filesDir; + } + + long getCurrentStorageFreeSpaceInBytes() { + return filesDir.getFreeSpace(); + } + + long getCurrentFreeHeapInBytes() { + Runtime runtime = Runtime.getRuntime(); + return runtime.freeMemory(); + } + + @Nullable + Double getCurrentBatteryPercent() { + return batteryPercent; + } + + @Override + public void onReceive(Context context, Intent intent) { + int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + batteryPercent = level * 100.0d / (float) scale; + } +} diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRum.java b/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRum.java index 7f190e1c2..c79312150 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRum.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRum.java @@ -17,6 +17,7 @@ package com.splunk.rum; import static io.opentelemetry.api.common.AttributeKey.doubleKey; +import static io.opentelemetry.api.common.AttributeKey.longKey; import static io.opentelemetry.api.common.AttributeKey.stringKey; import static java.util.Objects.requireNonNull; @@ -61,6 +62,10 @@ public class SplunkRum { static final AttributeKey LOCATION_LATITUDE_KEY = doubleKey("location.lat"); static final AttributeKey LOCATION_LONGITUDE_KEY = doubleKey("location.long"); + static final AttributeKey STORAGE_SPACE_FREE_KEY = longKey("storage.free"); + static final AttributeKey HEAP_FREE_KEY = longKey("heap.free"); + static final AttributeKey BATTERY_PERCENT_KEY = doubleKey("battery.percent"); + static final String COMPONENT_APPSTART = "appstart"; static final String COMPONENT_CRASH = "crash"; static final String COMPONENT_ERROR = "error"; diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/CrashReporterTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/CrashReporterTest.java index 63cb6803a..2d5d95033 100644 --- a/splunk-otel-android/src/test/java/com/splunk/rum/CrashReporterTest.java +++ b/splunk-otel-android/src/test/java/com/splunk/rum/CrashReporterTest.java @@ -45,13 +45,19 @@ public class CrashReporterTest { private Tracer tracer; private SdkTracerProvider sdkTracerProvider; private CompletableResultCode flushResult; + private RuntimeDetails runtimeDetails; @Before public void setup() { tracer = otelTesting.getOpenTelemetry().getTracer("testTracer"); sdkTracerProvider = mock(SdkTracerProvider.class); flushResult = mock(CompletableResultCode.class); + runtimeDetails = mock(RuntimeDetails.class); + when(sdkTracerProvider.forceFlush()).thenReturn(flushResult); + when(runtimeDetails.getCurrentBatteryPercent()).thenReturn(12.0d); + when(runtimeDetails.getCurrentFreeHeapInBytes()).thenReturn(1024 * 12L); + when(runtimeDetails.getCurrentStorageFreeSpaceInBytes()).thenReturn(1024 * 99L); } @Test @@ -59,7 +65,7 @@ public void crashReportingSpan() { TestDelegateHandler existingHandler = new TestDelegateHandler(); CrashReporter.CrashReportingExceptionHandler crashReporter = new CrashReporter.CrashReportingExceptionHandler( - tracer, sdkTracerProvider, existingHandler); + tracer, sdkTracerProvider, existingHandler, runtimeDetails); NullPointerException oopsie = new NullPointerException("oopsie"); Thread crashThread = new Thread("badThread"); @@ -75,7 +81,10 @@ public void crashReportingSpan() { equalTo(SemanticAttributes.THREAD_ID, crashThread.getId()), equalTo(SemanticAttributes.THREAD_NAME, "badThread"), equalTo(SemanticAttributes.EXCEPTION_ESCAPED, true), - equalTo(SplunkRum.COMPONENT_KEY, SplunkRum.COMPONENT_CRASH)) + equalTo(SplunkRum.COMPONENT_KEY, SplunkRum.COMPONENT_CRASH), + equalTo(SplunkRum.STORAGE_SPACE_FREE_KEY, 101376), + equalTo(SplunkRum.HEAP_FREE_KEY, 12288), + equalTo(SplunkRum.BATTERY_PERCENT_KEY, 12.0)) .hasException(oopsie) .hasStatus(StatusData.error()); @@ -87,7 +96,8 @@ public void crashReportingSpan() { @Test public void multipleErrorsDuringACrash() { CrashReporter.CrashReportingExceptionHandler crashReporter = - new CrashReporter.CrashReportingExceptionHandler(tracer, sdkTracerProvider, null); + new CrashReporter.CrashReportingExceptionHandler( + tracer, sdkTracerProvider, null, runtimeDetails); Exception firstError = new NullPointerException("boom!"); Thread crashThread = new Thread("crashThread"); @@ -107,7 +117,10 @@ public void multipleErrorsDuringACrash() { equalTo(SemanticAttributes.THREAD_ID, crashThread.getId()), equalTo(SemanticAttributes.THREAD_NAME, "crashThread"), equalTo(SemanticAttributes.EXCEPTION_ESCAPED, true), - equalTo(SplunkRum.COMPONENT_KEY, SplunkRum.COMPONENT_CRASH)) + equalTo(SplunkRum.COMPONENT_KEY, SplunkRum.COMPONENT_CRASH), + equalTo(SplunkRum.STORAGE_SPACE_FREE_KEY, 101376), + equalTo(SplunkRum.HEAP_FREE_KEY, 12288), + equalTo(SplunkRum.BATTERY_PERCENT_KEY, 12.0)) .hasException(firstError) .hasStatus(StatusData.error()); @@ -117,7 +130,10 @@ public void multipleErrorsDuringACrash() { equalTo(SemanticAttributes.THREAD_ID, anotherThread.getId()), equalTo(SemanticAttributes.THREAD_NAME, "someOtherThread"), equalTo(SemanticAttributes.EXCEPTION_ESCAPED, true), - equalTo(SplunkRum.COMPONENT_KEY, SplunkRum.COMPONENT_ERROR)) + equalTo(SplunkRum.COMPONENT_KEY, SplunkRum.COMPONENT_ERROR), + equalTo(SplunkRum.STORAGE_SPACE_FREE_KEY, 101376), + equalTo(SplunkRum.HEAP_FREE_KEY, 12288), + equalTo(SplunkRum.BATTERY_PERCENT_KEY, 12.0)) .hasException(secondError) .hasStatus(StatusData.error()); diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/RumInitializerTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/RumInitializerTest.java index 1d9e09a1f..bc2c0ba80 100644 --- a/splunk-otel-android/src/test/java/com/splunk/rum/RumInitializerTest.java +++ b/splunk-otel-android/src/test/java/com/splunk/rum/RumInitializerTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.when; import android.app.Application; +import android.content.Context; import android.os.Looper; import com.google.common.base.Strings; import io.opentelemetry.api.common.AttributeKey; @@ -54,6 +55,10 @@ public void initializationSpan() { .setApplicationName("testApp") .setRumAccessToken("accessToken"); Application application = mock(Application.class); + Context context = mock(Context.class); + + when(application.getApplicationContext()).thenReturn(context); + InMemorySpanExporter testExporter = InMemorySpanExporter.create(); AppStartupTimer startupTimer = new AppStartupTimer(); RumInitializer testInitializer = @@ -108,6 +113,10 @@ public void spanLimitsAreConfigured() { .setApplicationName("testApp") .setRumAccessToken("accessToken"); Application application = mock(Application.class); + Context context = mock(Context.class); + + when(application.getApplicationContext()).thenReturn(context); + InMemorySpanExporter testExporter = InMemorySpanExporter.create(); AppStartupTimer startupTimer = new AppStartupTimer(); RumInitializer testInitializer = @@ -209,6 +218,9 @@ public void shouldTranslateExceptionEventsToSpanAttributes() { .setApplicationName("test"); Application application = mock(Application.class); ConnectionUtil connectionUtil = mock(ConnectionUtil.class, RETURNS_DEEP_STUBS); + Context context = mock(Context.class); + + when(application.getApplicationContext()).thenReturn(context); when(connectionUtil.refreshNetworkStatus().isOnline()).thenReturn(true); ConnectionUtil.Factory connectionUtilFactory = mock(ConnectionUtil.Factory.class); when(connectionUtilFactory.createAndStart(application)).thenReturn(connectionUtil); diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/RuntimeDetailsTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/RuntimeDetailsTest.java new file mode 100644 index 000000000..d097176fe --- /dev/null +++ b/splunk-otel-android/src/test/java/com/splunk/rum/RuntimeDetailsTest.java @@ -0,0 +1,66 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.rum; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.Intent; +import android.os.BatteryManager; +import java.io.File; +import org.junit.Before; +import org.junit.Test; + +public class RuntimeDetailsTest { + + private Context context; + + @Before + public void setup() { + context = mock(Context.class); + } + + @Test + public void testBattery() { + Intent intent = mock(Intent.class); + + Integer level = 690; + when(intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)).thenReturn(level); + when(intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)).thenReturn(1000); + + RuntimeDetails details = RuntimeDetails.create(context); + + details.onReceive(context, intent); + Double result = details.getCurrentBatteryPercent(); + assertEquals(69.0d, result, 0.001); + } + + @Test + public void testFreeSpace() { + File filesDir = mock(File.class); + + when(context.getFilesDir()).thenReturn(filesDir); + when(filesDir.getFreeSpace()).thenReturn(4200L); + + RuntimeDetails details = RuntimeDetails.create(context); + + long result = details.getCurrentStorageFreeSpaceInBytes(); + assertEquals(4200L, result); + } +}