Skip to content

Commit

Permalink
Extract ANR detection instrumentation (#399)
Browse files Browse the repository at this point in the history
* Extract ANR detection instrumentation

* Code review comments
  • Loading branch information
Mateusz Rzeszutek authored Nov 4, 2022
1 parent eb439e9 commit 0af808d
Show file tree
Hide file tree
Showing 13 changed files with 487 additions and 134 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
package com.splunk.rum;

import static com.splunk.rum.SplunkRum.APP_NAME_KEY;
import static com.splunk.rum.SplunkRum.COMPONENT_ERROR;
import static com.splunk.rum.SplunkRum.COMPONENT_KEY;
import static com.splunk.rum.SplunkRum.LOG_TAG;
import static com.splunk.rum.SplunkRum.RUM_VERSION_KEY;
import static io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor.constant;
import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.DEPLOYMENT_ENVIRONMENT;
import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.DEVICE_MODEL_IDENTIFIER;
import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.DEVICE_MODEL_NAME;
Expand All @@ -29,7 +32,6 @@

import android.app.Application;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.NonNull;
Expand All @@ -43,7 +45,7 @@
import io.opentelemetry.rum.internal.GlobalAttributesSpanAppender;
import io.opentelemetry.rum.internal.OpenTelemetryRum;
import io.opentelemetry.rum.internal.OpenTelemetryRumBuilder;
import io.opentelemetry.rum.internal.instrumentation.ApplicationStateListener;
import io.opentelemetry.rum.internal.instrumentation.anr.AnrDetector;
import io.opentelemetry.sdk.common.Clock;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.resources.Resource;
Expand All @@ -58,9 +60,6 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.logging.Level;
Expand Down Expand Up @@ -157,8 +156,12 @@ SplunkRum initialize(ConnectionUtil.Factory connectionUtilFactory, Looper mainLo
if (builder.anrDetectionEnabled) {
otelRumBuilder.addInstrumentation(
instrumentedApplication -> {
instrumentedApplication.registerApplicationStateListener(
initializeAnrReporting(mainLooper));
AnrDetector.builder()
.addAttributesExtractor(constant(COMPONENT_KEY, COMPONENT_ERROR))
.setMainLooper(mainLooper)
.build()
.installOn(instrumentedApplication);

initializationEvents.add(
new RumInitializer.InitializationEvent(
"anrMonitorInitialized", timingClock.now()));
Expand Down Expand Up @@ -262,35 +265,6 @@ private SlowRenderingDetector buildSlowRenderingDetector(Tracer tracer) {
return new SlowRenderingDetectorImpl(tracer, builder.slowRenderingDetectionPollInterval);
}

private ApplicationStateListener initializeAnrReporting(Looper mainLooper) {
Thread mainThread = mainLooper.getThread();
Handler uiHandler = new Handler(mainLooper);
// TODO: this is hacky behavior that utilizes a mutable variable, fix this!
AnrWatcher anrWatcher = new AnrWatcher(uiHandler, mainThread, SplunkRum::getInstance);
ScheduledExecutorService anrScheduler = Executors.newScheduledThreadPool(1);
final ScheduledFuture<?> scheduledFuture =
anrScheduler.scheduleAtFixedRate(anrWatcher, 1, 1, TimeUnit.SECONDS);
return new ApplicationStateListener() {

@Nullable private ScheduledFuture<?> future = scheduledFuture;

@Override
public void onApplicationForegrounded() {
if (future == null) {
future = anrScheduler.scheduleAtFixedRate(anrWatcher, 1, 1, TimeUnit.SECONDS);
}
}

@Override
public void onApplicationBackgrounded() {
if (future != null) {
future.cancel(true);
future = null;
}
}
};
}

private String detectRumVersion() {
try {
// todo: figure out if there's a way to get access to resources from pure non-UI library
Expand Down
20 changes: 0 additions & 20 deletions splunk-otel-android/src/main/java/com/splunk/rum/SplunkRum.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,11 @@
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.instrumentation.okhttp.v3_0.OkHttpTelemetry;
import io.opentelemetry.rum.internal.GlobalAttributesSpanAppender;
import io.opentelemetry.rum.internal.OpenTelemetryRum;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import okhttp3.Call;
Expand Down Expand Up @@ -247,24 +245,6 @@ Tracer getTracer() {
return getOpenTelemetry().getTracer(RUM_TRACER_NAME);
}

void recordAnr(StackTraceElement[] stackTrace) {
getTracer()
.spanBuilder("ANR")
.setAttribute(SemanticAttributes.EXCEPTION_STACKTRACE, formatStackTrace(stackTrace))
.setAttribute(COMPONENT_KEY, COMPONENT_ERROR)
.startSpan()
.setStatus(StatusCode.ERROR)
.end();
}

private String formatStackTrace(StackTraceElement[] stackTrace) {
StringBuilder stringBuilder = new StringBuilder();
for (StackTraceElement stackTraceElement : stackTrace) {
stringBuilder.append(stackTraceElement).append("\n");
}
return stringBuilder.toString();
}

/**
* Set an attribute in the global attributes that will be appended to every span and event.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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 io.opentelemetry.rum.internal.instrumentation.anr;

import android.os.Handler;
import android.os.Looper;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.rum.internal.instrumentation.InstrumentedApplication;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;

/** Entrypoint for installing the ANR (application not responding) detection instrumentation. */
public final class AnrDetector {

/** Returns a new {@link AnrDetector} with the default settings. */
public static AnrDetector create() {
return builder().build();
}

/** Returns a new {@link AnrDetectorBuilder}. */
public static AnrDetectorBuilder builder() {
return new AnrDetectorBuilder();
}

private final List<AttributesExtractor<StackTraceElement[], Void>> additionalExtractors;
private final Looper mainLooper;
private final ScheduledExecutorService scheduler;

AnrDetector(AnrDetectorBuilder builder) {
this.additionalExtractors = builder.additionalExtractors;
this.mainLooper = builder.mainLooper;
this.scheduler = builder.scheduler;
}

/**
* Installs the ANR detection instrumentation on the given {@link InstrumentedApplication}.
*
* <p>When the main thread is unresponsive for 5 seconds or more, an event including the main
* thread's stack trace will be reported to the RUM system.
*/
public void installOn(InstrumentedApplication instrumentedApplication) {
Handler uiHandler = new Handler(mainLooper);
AnrWatcher anrWatcher =
new AnrWatcher(
uiHandler,
mainLooper.getThread(),
buildAnrInstrumenter(instrumentedApplication.getOpenTelemetrySdk()));

AnrDetectorToggler listener = new AnrDetectorToggler(anrWatcher, scheduler);
// call it manually the first time to enable the ANR detection
listener.onApplicationForegrounded();

instrumentedApplication.registerApplicationStateListener(listener);
}

private Instrumenter<StackTraceElement[], Void> buildAnrInstrumenter(
OpenTelemetry openTelemetry) {
return Instrumenter.<StackTraceElement[], Void>builder(
openTelemetry, "io.opentelemetry.anr", stackTrace -> "ANR")
// it's always an error
.setSpanStatusExtractor(
(spanStatusBuilder, stackTrace, unused, error) ->
spanStatusBuilder.setStatus(StatusCode.ERROR))
.addAttributesExtractor(new StackTraceFormatter())
.addAttributesExtractors(additionalExtractors)
.buildInstrumenter();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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 io.opentelemetry.rum.internal.instrumentation.anr;

import android.os.Looper;
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

/** A builder of {@link AnrDetector}. */
public final class AnrDetectorBuilder {

AnrDetectorBuilder() {}

final List<AttributesExtractor<StackTraceElement[], Void>> additionalExtractors =
new ArrayList<>();
Looper mainLooper = Looper.getMainLooper();
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

/** Adds an {@link AttributesExtractor} that will extract additional attributes. */
public AnrDetectorBuilder addAttributesExtractor(
AttributesExtractor<StackTraceElement[], Void> extractor) {
additionalExtractors.add(extractor);
return this;
}

/** Sets a custom {@link Looper} to run on. Useful for testing. */
public AnrDetectorBuilder setMainLooper(Looper looper) {
mainLooper = looper;
return this;
}

// visible for tests
AnrDetectorBuilder setScheduler(ScheduledExecutorService scheduler) {
this.scheduler = scheduler;
return this;
}

/** Returns a new {@link AnrDetector} with the settings of this {@link AnrDetectorBuilder}. */
public AnrDetector build() {
return new AnrDetector(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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 io.opentelemetry.rum.internal.instrumentation.anr;

import androidx.annotation.Nullable;
import io.opentelemetry.rum.internal.instrumentation.ApplicationStateListener;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

final class AnrDetectorToggler implements ApplicationStateListener {

private final Runnable anrWatcher;
private final ScheduledExecutorService anrScheduler;

@Nullable private ScheduledFuture<?> future;

AnrDetectorToggler(Runnable anrWatcher, ScheduledExecutorService anrScheduler) {
this.anrWatcher = anrWatcher;
this.anrScheduler = anrScheduler;
}

@Override
public void onApplicationForegrounded() {
if (future == null) {
future = anrScheduler.scheduleAtFixedRate(anrWatcher, 1, 1, TimeUnit.SECONDS);
}
}

@Override
public void onApplicationBackgrounded() {
if (future != null) {
future.cancel(true);
future = null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,28 @@
* limitations under the License.
*/

package com.splunk.rum;
package io.opentelemetry.rum.internal.instrumentation.anr;

import android.os.Handler;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

class AnrWatcher implements Runnable {
final class AnrWatcher implements Runnable {
private final AtomicInteger anrCounter = new AtomicInteger();
private final Handler uiHandler;
private final Thread mainThread;
private final Supplier<SplunkRum> splunkRumSupplier;
private final Instrumenter<StackTraceElement[], Void> instrumenter;

AnrWatcher(Handler uiHandler, Thread mainThread, Supplier<SplunkRum> splunkRumSupplier) {
AnrWatcher(
Handler uiHandler,
Thread mainThread,
Instrumenter<StackTraceElement[], Void> instrumenter) {
this.uiHandler = uiHandler;
this.mainThread = mainThread;
this.splunkRumSupplier = splunkRumSupplier;
this.instrumenter = instrumenter;
}

@Override
Expand All @@ -53,9 +57,14 @@ public void run() {
}
if (anrCounter.incrementAndGet() >= 5) {
StackTraceElement[] stackTrace = mainThread.getStackTrace();
splunkRumSupplier.get().recordAnr(stackTrace);
recordAnr(stackTrace);
// only report once per 5s.
anrCounter.set(0);
}
}

private void recordAnr(StackTraceElement[] stackTrace) {
Context context = instrumenter.start(Context.current(), stackTrace);
instrumenter.end(context, stackTrace, null, null);
}
}
Loading

0 comments on commit 0af808d

Please sign in to comment.