Skip to content

Commit

Permalink
Add runtime details to crash span (#369)
Browse files Browse the repository at this point in the history
* add runtime details to crash span

* make field volatile instead of atomicreference

* add nullable annotation

* fix tests
  • Loading branch information
breedx-splk authored Oct 5, 2022
1 parent 7f481fd commit 7f73c98
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 21 deletions.
43 changes: 28 additions & 15 deletions splunk-otel-android/src/main/java/com/splunk/rum/CrashReporter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,28 +28,32 @@

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
static class CrashReportingExceptionHandler implements Thread.UncaughtExceptionHandler {
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
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -61,6 +62,10 @@ public class SplunkRum {
static final AttributeKey<Double> LOCATION_LATITUDE_KEY = doubleKey("location.lat");
static final AttributeKey<Double> LOCATION_LONGITUDE_KEY = doubleKey("location.long");

static final AttributeKey<Long> STORAGE_SPACE_FREE_KEY = longKey("storage.free");
static final AttributeKey<Long> HEAP_FREE_KEY = longKey("heap.free");
static final AttributeKey<Double> BATTERY_PERCENT_KEY = doubleKey("battery.percent");

static final String COMPONENT_APPSTART = "appstart";
static final String COMPONENT_CRASH = "crash";
static final String COMPONENT_ERROR = "error";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,27 @@ 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
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");
Expand All @@ -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());

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 =
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit 7f73c98

Please sign in to comment.