Skip to content

Commit

Permalink
Merge pull request #58 from efryntov/beef-up-android-logging
Browse files Browse the repository at this point in the history
Beef up Android logging
  • Loading branch information
efryntov authored Nov 19, 2024
2 parents c7bd399 + 56ecdaa commit 9d4403d
Show file tree
Hide file tree
Showing 3 changed files with 315 additions and 167 deletions.
3 changes: 1 addition & 2 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@
<provider
android:name=".nativemodule.logging.LoggingContentProvider"
android:authorities="${applicationId}.log"
android:exported="false"
android:process=":LoggingContentProvider" />
android:exported="false"/>
<!-- Receiver for handling app updates -->
<receiver android:name=".nativemodule.ConduitUpdateReceiver"
android:exported="false">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
Expand All @@ -32,6 +33,7 @@

import java.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.Level;
Expand All @@ -42,128 +44,195 @@
import ca.psiphon.conduit.nativemodule.Constants;

public class LoggingContentProvider extends ContentProvider {
final static String TAG = LoggingContentProvider.class.getSimpleName();

public static final String LOG_FILE_NAME = "conduit_log";
private static final int LOG_FILE_SIZE = Constants.QUARTER_MB;
private static final int LOG_FILE_COUNT = 2;

private static Logger logger;

@Override
public boolean onCreate() {
return true;
}

private void initializeLogger() {
logger = Logger.getLogger(LoggingContentProvider.class.getName());
logger.setLevel(Level.ALL);
logger.setUseParentHandlers(false);
// delete all existing handlers if any
for (var handler : logger.getHandlers()) {
logger.removeHandler(handler);
final static String TAG = LoggingContentProvider.class.getSimpleName();

public static final String LOG_FILE_NAME = "conduit_log";
private static final int LOG_FILE_SIZE = Constants.QUARTER_MB;
private static final int LOG_FILE_COUNT = 2;

// URI matching constants
private static final String AUTHORITY_SUFFIX = ".log";
private static final String PATH_INSERT_LOGS = "insert";
private static final int MATCH_INSERT = 1;

private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private volatile Logger logger;
private final Object loggerLock = new Object();

@Override
public boolean onCreate() {
String authority = getContext().getPackageName() + AUTHORITY_SUFFIX;
uriMatcher.addURI(authority, PATH_INSERT_LOGS, MATCH_INSERT);
return true;
}

try {
File dataDir = ConduitModule.dataRootDirectory(getContext());
// Set up the FileHandler
FileHandler fileHandler = new FileHandler(
new File(dataDir, LOG_FILE_NAME).getAbsolutePath(),
LOG_FILE_SIZE,
LOG_FILE_COUNT,
true
);
fileHandler.setFormatter(new JsonFormatter());
fileHandler.setLevel(Level.ALL);
logger.addHandler(fileHandler);
} catch (IOException e) {
Log.e(TAG, "Failed to initialize logger", e);
private Logger getLogger() {
Logger result = logger;
if (result == null) {
synchronized (loggerLock) {
result = logger;
if (result == null) {
initializeLogger();
result = logger;
}
}
}
return result;
}
}

@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
if (logger == null) {
initializeLogger();
private void initializeLogger() {
logger = Logger.getLogger(LoggingContentProvider.class.getName());
logger.setLevel(Level.ALL);
logger.setUseParentHandlers(false);

// Clean up any existing handlers
for (var handler : logger.getHandlers()) {
try {
handler.close();
logger.removeHandler(handler);
} catch (Exception e) {
Log.e(TAG, "Error cleaning up handler", e);
}
}

try {
File dataDir = ConduitModule.dataRootDirectory(getContext());
// Set up the FileHandler
FileHandler fileHandler = new FileHandler(
new File(dataDir, LOG_FILE_NAME).getAbsolutePath(),
LOG_FILE_SIZE,
LOG_FILE_COUNT,
true
);
fileHandler.setFormatter(new JsonFormatter());
fileHandler.setLevel(Level.ALL);
logger.addHandler(fileHandler);
} catch (IOException e) {
Log.e(TAG, "Failed to initialize logger", e);
throw new IllegalStateException("Logger initialization failed", e);
}
}

String tag = values.getAsString("tag");
String message = values.getAsString("message");
int level = values.getAsInteger("level");

LogRecord record = new LogRecord(intToLevel(level), message);
record.setLoggerName(tag);
record.setMillis(values.getAsLong("timestamp"));

logger.log(record);

return uri;
}

private static Level intToLevel(int level) {
return switch (level) {
case Log.VERBOSE -> Level.FINEST;
case Log.DEBUG -> Level.FINE;
case Log.INFO -> Level.INFO;
case Log.WARN -> Level.WARNING;
case Log.ERROR -> Level.SEVERE;
default -> throw new IllegalArgumentException("Invalid log level: " + level);
};
}

private static String levelToString(Level level) {
if (level == Level.FINEST) {
return "Verbose";
} else if (level == Level.FINE) {
return "Debug";
} else if (level == Level.INFO) {
return "Info";
} else if (level == Level.WARNING) {
return "Warning";
} else if (level == Level.SEVERE) {
return "Error";
} else {
throw new IllegalArgumentException("Invalid log level: " + level);
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
// Validate URI
if (uriMatcher.match(uri) != MATCH_INSERT) {
throw new IllegalArgumentException("Unknown URI: " + uri);
}

// Validate input
if (values == null) {
throw new IllegalArgumentException("ContentValues cannot be null");
}

String tag = values.getAsString("tag");
String message = values.getAsString("message");
Integer level = values.getAsInteger("level");
Long timestamp = values.getAsLong("timestamp");

if (tag == null || message == null || level == null || timestamp == null) {
throw new IllegalArgumentException(
String.format(Locale.US, "Missing required fields. tag: %s, message: %s, level: %s, timestamp: %s",
tag != null ? "present" : "missing",
message != null ? "present" : "missing",
level != null ? "present" : "missing",
timestamp != null ? "present" : "missing"
)
);
}

Logger currentLogger = getLogger();
synchronized (loggerLock) {
LogRecord record = new LogRecord(intToLevel(level), message);
record.setLoggerName(tag);
record.setMillis(timestamp);
currentLogger.log(record);
return uri;
}
}
}

@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
String[] selectionArgs,
String sortOrder) {
throw new UnsupportedOperationException("Not implemented");
}

@Override
public String getType(@NonNull Uri uri) {
throw new UnsupportedOperationException("Not implemented");
}

@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("Not implemented");
}

@Override
public int update(@NonNull Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
throw new UnsupportedOperationException("Not implemented");
}

// Custom formatter to format logs as JSON, mimic the format of the tunnel core notices output
private static class JsonFormatter extends Formatter {

private static Level intToLevel(int level) {
return switch (level) {
case Log.VERBOSE -> Level.FINEST;
case Log.DEBUG -> Level.FINE;
case Log.INFO -> Level.INFO;
case Log.WARN -> Level.WARNING;
case Log.ERROR -> Level.SEVERE;
default -> throw new IllegalArgumentException("Invalid log level: " + level);
};
}

private static String levelToString(Level level) {
if (level == Level.FINEST) {
return "Verbose";
} else if (level == Level.FINE) {
return "Debug";
} else if (level == Level.INFO) {
return "Info";
} else if (level == Level.WARNING) {
return "Warning";
} else if (level == Level.SEVERE) {
return "Error";
} else {
throw new IllegalArgumentException("Invalid log level: " + level);
}
}

@Override
public String format(LogRecord record) {
JSONObject json = new JSONObject();
try {
json.put("tag", record.getLoggerName());
json.put("message", record.getMessage());
json.put("level", levelToString(record.getLevel()));
json.put("timestamp", LogUtils.getRfc3339Timestamp(record.getMillis()));
} catch (JSONException e) {
Log.e(TAG, "Failed to format log record", e);
}
return json + "\n";
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
String[] selectionArgs,
String sortOrder) {
throw new UnsupportedOperationException("Not implemented");
}

@Override
public String getType(@NonNull Uri uri) {
throw new UnsupportedOperationException("Not implemented");
}

@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("Not implemented");
}

@Override
public int update(@NonNull Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
throw new UnsupportedOperationException("Not implemented");
}

@Override
public void shutdown() {
synchronized (loggerLock) {
if (logger != null) {
for (var handler : logger.getHandlers()) {
try {
handler.close();
logger.removeHandler(handler);
} catch (Exception e) {
Log.e(TAG, "Error closing handler during shutdown", e);
}
}
logger = null;
}
}
super.shutdown();
}

// Custom formatter to format logs as JSON, mimic the format of the tunnel core notices output
private static class JsonFormatter extends Formatter {
@Override
public String format(LogRecord record) {
JSONObject json = new JSONObject();
try {
json.put("tag", record.getLoggerName());
json.put("message", record.getMessage());
json.put("level", levelToString(record.getLevel()));
json.put("timestamp", LogUtils.getRfc3339Timestamp(record.getMillis()));
} catch (JSONException e) {
Log.e(TAG, "Failed to format log record", e);
}
return json + "\n";
}
}
}
}
Loading

0 comments on commit 9d4403d

Please sign in to comment.