Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
Comment thread
brianr marked this conversation as resolved.
Outdated
}

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