Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions rollbar-android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ android {
}
}

testOptions {
unitTests.isReturnDefaultValues = true
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package com.rollbar.android;

import com.rollbar.android.anr.AnrConfiguration;
import com.rollbar.api.payload.data.Level;

public class AndroidConfiguration {
private final AnrConfiguration anrConfiguration;
private final boolean mustCaptureNavigationEvents;
private final boolean mustCaptureLogsAsTelemetry;
private final Level minimumLogCaptureLevel;

AndroidConfiguration(Builder builder) {
anrConfiguration = builder.anrConfiguration;
mustCaptureNavigationEvents = builder.mustCaptureNavigationEvents;
mustCaptureLogsAsTelemetry = builder.mustCaptureLogsAsTelemetry;
minimumLogCaptureLevel = builder.minimumLogCaptureLevel;
}

public AnrConfiguration getAnrConfiguration() {
Expand All @@ -19,10 +24,20 @@ public boolean mustCaptureNavigationEvents() {
return mustCaptureNavigationEvents;
}

public boolean mustCaptureLogsAsTelemetry() {
return mustCaptureLogsAsTelemetry;
}

public Level getMinimumLogCaptureLevel() {
return minimumLogCaptureLevel;
}


public static final class Builder {
private AnrConfiguration anrConfiguration;
private boolean mustCaptureNavigationEvents = true;
private boolean mustCaptureLogsAsTelemetry = false;
private Level minimumLogCaptureLevel = Level.WARNING;

public Builder() {
anrConfiguration = new AnrConfiguration.Builder().build();
Expand All @@ -49,6 +64,32 @@ public Builder captureNewActivityTelemetryEvents(boolean mustCaptureNavigationEv
return this;
}

/**
* Enable or disable automatic capture of Android log output as telemetry events.
* When enabled, logs emitted via {@code android.util.Log} (and any other source written to
* logcat from this app's UID, including third-party libraries) at or above the configured
* minimum level are recorded as log telemetry events with
* {@link com.rollbar.api.payload.data.Source#CLIENT}.
* Default is disabled.
* @param mustCaptureLogsAsTelemetry if automatic capture must be enabled or disabled.
* @return the builder instance
*/
Comment thread
claude[bot] marked this conversation as resolved.
public Builder captureLogsAsTelemetry(boolean mustCaptureLogsAsTelemetry) {
this.mustCaptureLogsAsTelemetry = mustCaptureLogsAsTelemetry;
return this;
}

/**
* Minimum log level to capture as telemetry when {@link #captureLogsAsTelemetry(boolean)}
* is enabled. Default is {@link Level#WARNING}.
* @param minimumLogCaptureLevel the minimum level (inclusive) to capture.
* @return the builder instance
*/
public Builder minimumLogCaptureLevel(Level minimumLogCaptureLevel) {
this.minimumLogCaptureLevel = minimumLogCaptureLevel;
return this;
}

public AndroidConfiguration build() {
return new AndroidConfiguration(this);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package com.rollbar.android;

import android.util.Log;

import com.rollbar.api.payload.data.Level;
import com.rollbar.api.payload.data.Source;
import com.rollbar.notifier.telemetry.TelemetryEventTracker;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

class LogcatTelemetryCapture {

// threadtime format: "MM-dd HH:mm:ss.SSS PID TID L Tag: message"
private static final Pattern LOGCAT_LINE_PATTERN = Pattern.compile(
"^\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3}\\s+\\d+\\s+\\d+\\s+([VDIWEF])\\s+(.+?):\\s(.*)$"
);

private final TelemetryEventTracker tracker;
private final Level minimumLevel;
private final String selfTag;
private final ProcessFactory processFactory;

private Thread thread;
private Process process;
private volatile boolean running;

LogcatTelemetryCapture(
TelemetryEventTracker tracker,
Level minimumLevel,
String selfTag
) {
this(tracker, minimumLevel, selfTag, defaultProcessFactory());
}

LogcatTelemetryCapture(
TelemetryEventTracker tracker,
Level minimumLevel,
String selfTag,
ProcessFactory processFactory
) {
this.tracker = tracker;
this.minimumLevel = minimumLevel != null ? minimumLevel : Level.WARNING;
this.selfTag = selfTag;
this.processFactory = processFactory;
}

synchronized void start() {
if (running) {
return;
}
try {
this.process = processFactory.start(logcatPriorityFor(this.minimumLevel));
} catch (IOException e) {
Log.w(Rollbar.TAG, "Failed to start logcat telemetry capture", e);
return;
}
running = true;
thread = new Thread(new Runnable() {
@Override
public void run() {
readLoop();
}
}, "rollbar-logcat-telemetry");
thread.setDaemon(true);
thread.start();
}

synchronized void stop() {
if (!running) {
return;
}
running = false;
if (process != null) {
process.destroy();
process = null;
}
if (thread != null) {
thread.interrupt();
thread = null;
}
}

private void readLoop() {
Process currentProcess = this.process;
if (currentProcess == null) {
return;
}
BufferedReader reader = new BufferedReader(
new InputStreamReader(currentProcess.getInputStream(), Charset.forName("UTF-8")));
try {
String line;
while (running && (line = reader.readLine()) != null) {
processLine(line);
}
} catch (IOException e) {
// Process died or was destroyed — expected on stop().
} finally {
try {
reader.close();
} catch (IOException ignored) {
}
if (running) {
Log.w(Rollbar.TAG, "logcat process exited unexpectedly; resetting capture state");
stop();
}
}
}
Comment thread
buongarzoni marked this conversation as resolved.

void processLine(String line) {
if (line == null) {
return;
}
Matcher matcher = LOGCAT_LINE_PATTERN.matcher(line);
if (!matcher.matches()) {
return;
}

String priority = matcher.group(1);
String tag = matcher.group(2).trim();
String message = matcher.group(3);

if (selfTag != null && selfTag.equals(tag)) {
return;
}
Comment thread
buongarzoni marked this conversation as resolved.

Level level = mapPriorityToLevel(priority);
if (level == null) {
return;
}
if (level.level() < minimumLevel.level()) {
return;
}

try {
tracker.recordLogEventFor(level, Source.CLIENT, message);
} catch (Exception e) {
Comment thread
buongarzoni marked this conversation as resolved.
// Never let a broken tracker kill the reader thread.
}
}

static Level mapPriorityToLevel(String priority) {
if (priority == null || priority.isEmpty()) {
return null;
}
switch (priority.charAt(0)) {
case 'V':
case 'D':
return Level.DEBUG;
case 'I':
return Level.INFO;
case 'W':
return Level.WARNING;
case 'E':
return Level.ERROR;
case 'F':
return Level.CRITICAL;
default:
return null;
}
}

static String logcatPriorityFor(Level level) {
if (level == null) {
return "W";
}
switch (level) {
case DEBUG:
return "V";
case INFO:
return "I";
case WARNING:
return "W";
case ERROR:
return "E";
case CRITICAL:
return "F";
default:
return "W";
}
}
Comment thread
buongarzoni marked this conversation as resolved.

interface ProcessFactory {
Process start(String priorityFilter) throws IOException;
}

private static ProcessFactory defaultProcessFactory() {
return new ProcessFactory() {
@Override
public Process start(String priorityFilter) throws IOException {
return new ProcessBuilder(
"logcat", "-v", "threadtime", "-T", "1", "*:" + priorityFilter)
.redirectErrorStream(true)
.start();
}
};
Comment thread
buongarzoni marked this conversation as resolved.
}
}
36 changes: 36 additions & 0 deletions rollbar-android/src/main/java/com/rollbar/android/Rollbar.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public class Rollbar implements Closeable {
private final ConnectionAwareSenderFailureStrategy senderFailureStrategy;

private com.rollbar.notifier.Rollbar rollbar;
private LogcatTelemetryCapture logcatTelemetryCapture;
private static Rollbar notifier;

private final int versionCode;
Expand Down Expand Up @@ -236,6 +237,7 @@ public static Rollbar init(
if (androidConfiguration != null) {
initAnrDetector(context, androidConfiguration);
initAutomaticCaptureOfNavigationTelemetryEvents(context, androidConfiguration);
initAutomaticCaptureOfLogTelemetryEvents(androidConfiguration);
}
}

Expand Down Expand Up @@ -277,12 +279,21 @@ public static Rollbar init(Context context, ConfigProvider provider) {
AndroidConfiguration androidConfiguration = makeDefaultAndroidConfiguration();
initAnrDetector(context, androidConfiguration);
initAutomaticCaptureOfNavigationTelemetryEvents(context, androidConfiguration);
initAutomaticCaptureOfLogTelemetryEvents(androidConfiguration);
}
return notifier;
}

@Override
public void close() throws IOException {
if (logcatTelemetryCapture != null) {
try {
logcatTelemetryCapture.stop();
} catch (Exception e) {
Log.w(TAG, "Error stopping logcat telemetry capture", e);
}
logcatTelemetryCapture = null;
}
if (rollbar != null) {
try {
rollbar.close(false);
Expand Down Expand Up @@ -1202,6 +1213,31 @@ private static void initAutomaticCaptureOfNavigationTelemetryEvents(
}
}

private static void initAutomaticCaptureOfLogTelemetryEvents(
AndroidConfiguration androidConfiguration
) {
if (!androidConfiguration.mustCaptureLogsAsTelemetry()) {
return;
}

com.rollbar.notifier.Rollbar rollbarNotifier = notifier.rollbar;
if (rollbarNotifier == null) {
return;
}

TelemetryEventTracker telemetryEventTracker = rollbarNotifier.getTelemetryEventTracker();
if (telemetryEventTracker == null) {
return;
}

LogcatTelemetryCapture logcatTelemetryCapture = new LogcatTelemetryCapture(
telemetryEventTracker,
androidConfiguration.getMinimumLogCaptureLevel(),
TAG);
logcatTelemetryCapture.start();
notifier.logcatTelemetryCapture = logcatTelemetryCapture;
}

private String loadAccessTokenFromManifest(Context context) throws NameNotFoundException {
Context appContext = context.getApplicationContext();
ApplicationInfo ai = appContext.getPackageManager().getApplicationInfo(appContext.getPackageName(), PackageManager.GET_META_DATA);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import android.net.NetworkInfo;
import android.os.Bundle;
import android.util.Log;
import com.rollbar.android.Rollbar;
import com.rollbar.notifier.util.ObjectsUtils;

import java.io.Closeable;
Expand Down Expand Up @@ -46,7 +47,7 @@ public void updateContext(Context androidContext) {
String message = "This application is missing the " +
"android.permission.ACCESS_NETWORK_STATE permission. The Rollbar notifier " +
"will *not* be able to detect when the network is unavailable.";
Log.w(ConnectivityDetector.class.getCanonicalName(), message);
Log.w(Rollbar.TAG, message);
}
}

Expand Down
Loading
Loading