ch.qos.logback.classic.android.SQLiteAppender Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of logback-android-classic Show documentation
Show all versions of logback-android-classic Show documentation
Advanced logging for Android apps
/**
* Logback: the reliable, generic, fast and flexible logging framework.
* Copyright (C) 1999-2013, QOS.ch. All rights reserved.
*
* This program and the accompanying materials are dual-licensed under
* either the terms of the Eclipse Public License v1.0 as published by
* the Eclipse Foundation
*
* or (per the licensee's choosing)
*
* under the terms of the GNU Lesser General Public License version 2.1
* as published by the Free Software Foundation.
*/
package ch.qos.logback.classic.android;
import java.io.File;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteStatement;
import ch.qos.logback.classic.db.SQLBuilder;
import ch.qos.logback.classic.db.names.DBNameResolver;
import ch.qos.logback.classic.db.names.DefaultDBNameResolver;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.StackTraceElementProxy;
import ch.qos.logback.classic.spi.ThrowableProxyUtil;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import ch.qos.logback.core.android.CommonPathUtil;
/**
* SQLiteAppender is a logback appender optimized for Android SQLite. It requires no JDBC
* as it uses the built-in Android SQLite API.
*
* @author Anthony Trinh
* @since 1.0.11
*/
public class SQLiteAppender extends UnsynchronizedAppenderBase {
private SQLiteDatabase db;
private String insertPropertiesSQL;
private String insertExceptionSQL;
private String insertSQL;
private DBNameResolver dbNameResolver;
/**
* Sets the database name resolver, used to customize the names of the table names
* and columns in the database.
*
* @param dbNameResolver the desired database name resolver
*/
public void setDbNameResolver(DBNameResolver dbNameResolver) {
this.dbNameResolver = dbNameResolver;
}
/*
* (non-Javadoc)
* @see ch.qos.logback.core.UnsynchronizedAppenderBase#start()
*/
@Override
public void start() {
this.started = false;
String packageName = null;
if (getContext() != null) {
packageName = getContext().getProperty(CoreConstants.PACKAGE_NAME_KEY);
}
if (packageName == null || packageName.length() == 0) {
addError("Cannot create database without package name");
return;
}
boolean dbOpened = false;
try {
File dbfile = new File(CommonPathUtil.getDatabaseDirectoryPath(packageName), "logback.db");
dbfile.getParentFile().mkdirs();
this.db = SQLiteDatabase.openOrCreateDatabase(dbfile.getPath(), null);
dbOpened = true;
} catch (SQLiteException e) {
addError("Cannot open database", e);
}
if (dbOpened) {
if (dbNameResolver == null) {
dbNameResolver = new DefaultDBNameResolver();
}
insertExceptionSQL = SQLBuilder.buildInsertExceptionSQL(dbNameResolver);
insertPropertiesSQL = SQLBuilder.buildInsertPropertiesSQL(dbNameResolver);
insertSQL = SQLBuilder.buildInsertSQL(dbNameResolver);
try {
this.db.execSQL(SQLBuilder.buildCreateLoggingEventTableSQL(dbNameResolver));
this.db.execSQL(SQLBuilder.buildCreatePropertyTableSQL(dbNameResolver));
this.db.execSQL(SQLBuilder.buildCreateExceptionTableSQL(dbNameResolver));
super.start();
this.started = true;
} catch (SQLiteException e) {
addError("Cannot create database tables", e);
}
}
}
/*
* (non-Javadoc)
* @see java.lang.Object#finalize()
*/
@Override
protected void finalize() throws Throwable {
this.db.close();
}
/*
* (non-Javadoc)
* @see ch.qos.logback.core.UnsynchronizedAppenderBase#stop()
*/
@Override
public void stop() {
this.db.close();
}
/*
* (non-Javadoc)
* @see ch.qos.logback.core.UnsynchronizedAppenderBase#append(java.lang.Object)
*/
@Override
public void append(ILoggingEvent eventObject) {
if (isStarted()) {
try {
SQLiteStatement stmt = db.compileStatement(insertSQL);
try {
db.beginTransaction();
long eventId = subAppend(eventObject, stmt);
if (eventId != -1) {
secondarySubAppend(eventObject, eventId);
db.setTransactionSuccessful();
}
} finally {
if (db.inTransaction()) {
db.endTransaction();
}
stmt.close();
}
} catch (Throwable e) {
addError("Cannot append event", e);
}
}
}
/**
* Inserts the main details of a log event into the database
*
* @param event the event to insert
* @param insertStatement the SQLite statement used to insert the event
* @return the row ID of the newly inserted event; -1 if the insertion failed
* @throws SQLException
*/
private long subAppend(ILoggingEvent event, SQLiteStatement insertStatement) throws SQLException {
bindLoggingEvent(insertStatement, event);
bindLoggingEventArguments(insertStatement, event.getArgumentArray());
// This is expensive... should we do it every time?
bindCallerData(insertStatement, event.getCallerData());
long insertId = -1;
try {
insertId = insertStatement.executeInsert();
} catch (SQLiteException e) {
addWarn("Failed to insert loggingEvent", e);
}
return insertId;
}
/**
* Updates an existing row of an event with the secondary details of the event.
* This includes MDC properties and any exception information.
*
* @param event the event containing the details to insert
* @param eventId the row ID of the event to modify
* @throws SQLException
*/
private void secondarySubAppend(ILoggingEvent event, long eventId) throws SQLException {
Map mergedMap = mergePropertyMaps(event);
insertProperties(mergedMap, eventId);
if (event.getThrowableProxy() != null) {
insertThrowable(event.getThrowableProxy(), eventId);
}
}
private static final int TIMESTMP_INDEX = 1;
private static final int FORMATTED_MESSAGE_INDEX = 2;
private static final int LOGGER_NAME_INDEX = 3;
private static final int LEVEL_STRING_INDEX = 4;
private static final int THREAD_NAME_INDEX = 5;
private static final int REFERENCE_FLAG_INDEX = 6;
private static final int ARG0_INDEX = 7;
// private static final int ARG1_INDEX = 8;
// private static final int ARG2_INDEX = 9;
// private static final int ARG3_INDEX = 10;
private static final int CALLER_FILENAME_INDEX = 11;
private static final int CALLER_CLASS_INDEX = 12;
private static final int CALLER_METHOD_INDEX = 13;
private static final int CALLER_LINE_INDEX = 14;
// private static final int EVENT_ID_INDEX = 15;
/**
* Binds the main details of a log event to a SQLite statement's parameters
*
* @param stmt the SQLite statement to modify
* @param event the event containing the details to bind
* @throws SQLException
*/
private void bindLoggingEvent(SQLiteStatement stmt, ILoggingEvent event) throws SQLException {
stmt.bindLong(TIMESTMP_INDEX, event.getTimeStamp());
stmt.bindString(FORMATTED_MESSAGE_INDEX, event.getFormattedMessage());
stmt.bindString(LOGGER_NAME_INDEX, event.getLoggerName());
stmt.bindString(LEVEL_STRING_INDEX, event.getLevel().toString());
stmt.bindString(THREAD_NAME_INDEX, event.getThreadName());
stmt.bindLong(REFERENCE_FLAG_INDEX, computeReferenceMask(event));
}
/**
* Binds a logging event's arguments (e.g., logger.debug("x={} y={}", arg1, arg2)
)
* to a SQLite statement's parameters
*
* @param stmt the SQLite statement to modify
* @param argArray the argument array to bind
* @throws SQLException
*/
private void bindLoggingEventArguments(SQLiteStatement stmt, Object[] argArray) throws SQLException {
int arrayLen = argArray != null ? argArray.length : 0;
for (int i = 0; i < arrayLen && i < 4; i++) {
stmt.bindString(ARG0_INDEX+i, asStringTruncatedTo254(argArray[i]));
}
//
// // set remaining columns to ""
// for (int i = arrayLen; i < 4; i++) {
// stmt.bindString(ARG0_INDEX+i, "");
// }
}
/**
* Gets the first 254 characters of an object's string representation. This is
* used to truncate a logging event's argument binding if necessary.
*
* @param o the object
* @return up to 254 characters of the object's string representation; or empty
* string if the object string is itself null
*/
private String asStringTruncatedTo254(Object o) {
String s = null;
if (o != null) {
s = o.toString();
}
if (s != null && s.length() > 254) {
s = s.substring(0, 254);
}
return s == null ? "" : s;
}
private static final short PROPERTIES_EXIST = 0x01;
private static final short EXCEPTION_EXISTS = 0x02;
/**
* Computes the reference mask for a logging event, including
* flags to indicate whether MDC properties or exception info
* is available for the event.
*
* @param event the logging event to evaluate
* @return the 16-bit reference mask
*/
private static short computeReferenceMask(ILoggingEvent event) {
short mask = 0;
int mdcPropSize = 0;
if (event.getMDCPropertyMap() != null) {
mdcPropSize = event.getMDCPropertyMap().keySet().size();
}
int contextPropSize = 0;
if (event.getLoggerContextVO().getPropertyMap() != null) {
contextPropSize = event.getLoggerContextVO().getPropertyMap().size();
}
if (mdcPropSize > 0 || contextPropSize > 0) {
mask = PROPERTIES_EXIST;
}
if (event.getThrowableProxy() != null) {
mask |= EXCEPTION_EXISTS;
}
return mask;
}
/**
* Merges a log event's properties with the properties of the logger context.
* The context properties are first in the map, and then the event's properties
* are appended.
*
* @param event the logging event to evaluate
* @return the merged properties map
*/
private Map mergePropertyMaps(ILoggingEvent event) {
Map mergedMap = new HashMap();
// we add the context properties first, then the event properties, since
// we consider that event-specific properties should have priority over
// context-wide properties.
Map loggerContextMap = event.getLoggerContextVO().getPropertyMap();
if (loggerContextMap != null) {
mergedMap.putAll(loggerContextMap);
}
Map mdcMap = event.getMDCPropertyMap();
if (mdcMap != null) {
mergedMap.putAll(mdcMap);
}
return mergedMap;
}
/**
* Updates an existing row with property details (context properties and event's properties).
*
* @param mergedMap the properties of the context plus the event's properties
* @param eventId the row ID of the event
* @throws SQLException
*/
private void insertProperties(Map mergedMap, long eventId) throws SQLException {
if (mergedMap.size() > 0) {
SQLiteStatement stmt = db.compileStatement(insertPropertiesSQL);
try {
for (Entry entry : mergedMap.entrySet()) {
stmt.bindLong(1, eventId);
stmt.bindString(2, entry.getKey());
stmt.bindString(3, entry.getValue());
stmt.executeInsert();
}
} finally {
stmt.close();
}
}
}
/**
* Binds the calling function's details (filename, line, etc.) to a SQLite statement's arguments
*
* @param stmt the SQLite statement to modify
* @param callerDataArray the caller's stack trace
* @throws SQLException
*/
private void bindCallerData(SQLiteStatement stmt, StackTraceElement[] callerDataArray) throws SQLException {
if (callerDataArray != null && callerDataArray.length > 0) {
StackTraceElement callerData = callerDataArray[0];
if (callerData != null) {
stmt.bindString(CALLER_FILENAME_INDEX, callerData.getFileName());
stmt.bindString(CALLER_CLASS_INDEX, callerData.getClassName());
stmt.bindString(CALLER_METHOD_INDEX, callerData.getMethodName());
stmt.bindString(CALLER_LINE_INDEX, Integer.toString(callerData.getLineNumber()));
}
}
}
/**
* Inserts an exception into the logging_exceptions table
*
* @param stmt
* @param txt
* @param i
* @param eventId
*/
private void insertException(SQLiteStatement stmt, String txt, short i, long eventId) throws SQLException {
stmt.bindLong(1, eventId);
stmt.bindLong(2, i);
stmt.bindString(3, txt);
stmt.executeInsert();
}
private void insertThrowable(IThrowableProxy tp, long eventId) throws SQLException {
SQLiteStatement stmt = db.compileStatement(insertExceptionSQL);
try {
short baseIndex = 0;
while (tp != null) {
StringBuilder buf = new StringBuilder();
ThrowableProxyUtil.subjoinFirstLine(buf, tp);
insertException(stmt, buf.toString(), baseIndex++, eventId);
int commonFrames = tp.getCommonFrames();
StackTraceElementProxy[] stepArray = tp.getStackTraceElementProxyArray();
for (int i = 0; i < stepArray.length - commonFrames; i++) {
StringBuilder sb = new StringBuilder();
sb.append(CoreConstants.TAB);
ThrowableProxyUtil.subjoinSTEP(sb, stepArray[i]);
insertException(stmt, sb.toString(), baseIndex++, eventId);
}
if (commonFrames > 0) {
StringBuilder sb = new StringBuilder();
sb.append(CoreConstants.TAB)
.append("... ")
.append(commonFrames)
.append(" common frames omitted");
insertException(stmt, sb.toString(), baseIndex++, eventId);
}
tp = tp.getCause();
}
} finally {
stmt.close();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy