-
-
Notifications
You must be signed in to change notification settings - Fork 173
Appender Notes
You can use the <lazy>
flag to allow lazy initialization (where the file is not created until the first write).
Use absolute paths to a file destination where your application has write-permissions, such as /data/data/<packagename>/files/
.
By default, an Android application has full permissions (read, write, execute, delete) for its own files only, which means other applications can't [easily] read your files (which is meant to protect your files from malicious apps). logback-android
does not change the permissions of the files it creates/writes. That is left to your application to manage. You could also subclass FileAppender
(or roll your own) to change your log's file permissions.
Example: Single file
<configuration>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<!-- lazy initialization: don't create the file until 1st write -->
<lazy>true</lazy>
<file>/data/data/com.example/files/log.txt</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration>
For rolling files, the file is rolled upon a log event past the rollover period (not automatically on time). This means that if no log events occur after 23 hours 59 minutes for a daily rollover period, the log file is not rolled over.
The <fileNamePattern>
must be an absolute path to a writeable location (also true for <file>
element). They don't have to be the same location (but typically are).
If an exception occurs during the rollover (e.g., the file could not be renamed), loggers will continue to write into the active log file, and the error is silently discarded. To troubleshoot these kinds of rollover problems, turn on debugging to show the rollover action as well as any resulting exceptions.
Example: Rolling file with a daily rollover
<configuration>
<property name="LOG_DIR" value="/sdcard/logback" />
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- active log file -->
<file>${LOG_DIR}/log.txt</file>
<encoder>
<pattern>%logger{35} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover period -->
<fileNamePattern>${LOG_DIR}/log.%d.txt</fileNamePattern>
<!-- keep 7 days' worth of history -->
<maxHistory>7</maxHistory>
</rollingPolicy>
</appender>
<root level="TRACE">
<appender-ref ref="FILE" />
</root>
</configuration>
Example: Rolling file with a daily rollover
private void configureLogbackDirectly() {
// reset the default context (which may already have been initialized)
// since we want to reconfigure it
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
context.stop();
final String LOG_DIR = "/sdcard/logback";
RollingFileAppender<ILoggingEvent> rollingFileAppender = new RollingFileAppender<ILoggingEvent>();
rollingFileAppender.setAppend(true);
rollingFileAppender.setContext(context);
// OPTIONAL: Set an active log file (separate from the rollover files).
// If rollingPolicy.fileNamePattern already set, you don't need this.
rollingFileAppender.setFile(LOG_DIR + "/log.txt");
TimeBasedRollingPolicy<ILoggingEvent> rollingPolicy = new TimeBasedRollingPolicy<ILoggingEvent>();
rollingPolicy.setFileNamePattern(LOG_DIR + "/log.%d.txt");
rollingPolicy.setMaxHistory(7);
rollingPolicy.setParent(rollingFileAppender); // parent and context required!
rollingPolicy.setContext(context);
rollingPolicy.start();
rollingFileAppender.setRollingPolicy(rollingPolicy);
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setPattern("%logger{35} - %msg%n");
encoder.setContext(context);
encoder.start();
rollingFileAppender.setEncoder(encoder);
rollingFileAppender.start();
// add the newly created appenders to the root logger;
// qualify Logger to disambiguate from org.slf4j.Logger
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
root.setLevel(Level.TRACE);
root.addAppender(rollingFileAppender);
// print any status messages (warnings, etc) encountered in logback config
StatusPrinter.print(context);
}
This appender requires the INTERNET
permission.
The SMTPAppender
sends emails silently (and asynchronously) on Android. No user interaction is required.
To use the SMTPAppender
, add the following dependencies:
// app/build.gradle
dependencies {
compile 'com.sun.mail:android-mail:1.5.5'
compile 'com.sun.mail:android-activation:1.5.5'
}
Otherwise, the appender will not initialize properly, and the following error will be seen in logcat during configuration:
I/System.out(919): 17:44:30,811 |-ERROR in ch.qos.logback.core.joran.action.AppenderAction - Could not create an Appender of type [ch.qos.logback.classic.net.SMTPAppender]. ch.qos.logback.core.util.DynamicClassLoadingException: Failed to instantiate type ch.qos.logback.classic.net.SMTPAppender
I/System.out(919): at ch.qos.logback.core.util.DynamicClassLoadingException: Failed to instantiate type ch.qos.logback.classic.net.SMTPAppender
[snip...]
Example:
<configuration>
<appender name="EMAIL" class="ch.qos.logback.classic.net.SMTPAppender">
<evaluator class="ch.qos.logback.classic.boolex.OnMarkerEvaluator">
<marker>NOTIFY_ADMIN</marker>
</evaluator>
<cyclicBufferTracker class="ch.qos.logback.core.spi.CyclicBufferTrackerImpl">
<!-- send 10 log entries per email -->
<bufferSize>10</bufferSize>
</cyclicBufferTracker>
<smtpHost>smtp.gmail.com</smtpHost>
<smtpPort>465</smtpPort>
<SSL>true</SSL>
<username>[email protected]</username>
<password>foo123</password>
<to>[email protected]</to>
<from>[email protected]</from>
<subject>%date{yyyyMMdd'T'HH:mm:ss.SSS}; %-5level; %msg</subject>
<layout class="ch.qos.logback.classic.html.HTMLLayout" />
</appender>
<root level="INFO">
<appender-ref ref="EMAIL" />
</root>
</configuration>
…accompanied by Android activity:
public class EmailActivity extends Activity {
static private final Logger LOG = LoggerFactory.getLogger(EmailActivity.class);
static private final Marker MARKER = MarkerFactory.getMarker("NOTIFY_ADMIN");
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Get the button declared in our layout.xml, and
// set its click-event handler to log error messages.
final Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// Since the appender was configured with a buffer size
// of 10, we'll get 2 emails with 10 log entries each:
// one with 1-10, and another with 11-20. The last 5
// entries won't be emailed until another 5 entries
// are logged.
for (int i = 0; i < 25; i++) {
LOG.info(MARKER, "i = {}", i);
}
}
});
LOG.info("end of onCreate");
}
}
This appender requires the INTERNET
permission.
Configuration can fail with the NetworkOnMainThreadException
if using these appenders to log messages from the main thread. To avoid this, use the AsyncAppender
with these appenders, and set the <lazy>
flag to not open the socket until the first write. Also be sure to stop the context in Activity#onDestroy()
as noted below.
Example:
<configuration>
<appender name="SOCKET" class="ch.qos.logback.classic.net.SocketAppender">
<!-- lazy initialization: don't open socket until 1st write -->
<lazy>true</lazy>
<remoteHost>${host}</remoteHost>
<port>${port}</port>
<reconnectionDelay>10000</reconnectionDelay>
<includeCallerData>${includeCallerData}</includeCallerData>
</appender>
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="SOCKET" />
</appender>
<root level="DEBUG">
<appender-ref ref="ASYNC" />
</root>
</configuration>
SQLiteAppender
automatically creates three tables (if nonexistent) in a SQLite database: logging_event
, logging_event_property
and logging_event_exception
.
The logging_event
table contains the following fields:
Field | Type | Description |
---|---|---|
timestamp |
big int |
The timestamp that was valid at the logging event's creation. |
formatted_message |
text |
The message that has been added to the logging event, after formatting with org.slf4j.impl.MessageFormatter , in case objects were passed along with the message. |
logger_name |
varchar |
The name of the logger used to issue the logging request. |
level_string |
varchar |
The level of the logging event. |
reference_flag |
smallint |
This field is used by logback to identify logging events that have an exception or MDC property values associated. Its value is computed by ch.qos.logback.classic.db.DBHelper . A logging event that contains MDC or Context properties has a flag number of 1 . One that contains an exception has a flag number of 2 . A logging event that contains both elements has a flag number of 3 . |
caller_filename |
varchar |
The name of the file where the logging request was issued. |
caller_class |
varchar |
The class where the logging request was issued. |
caller_method |
varchar |
The name of the method where the logging request was issued. |
caller_line |
char |
The line number where the logging request was issued. |
event_id |
int |
The database id of the logging event. |
The logging_event_property
is used to store the keys and values contained in the MDC or the Context. It contains these fields:
Field | Type | Description |
---|---|---|
event_id |
int |
The database id of the logging event. |
mapped_key |
varchar |
The key of the MDC property |
mapped_value |
text |
The value of the MDC property |
The logging_event_exception
table contains the following fields:
Field | Type | Description |
---|---|---|
event_id |
int |
The database id of the logging event. |
i |
smallint |
The index of the line in the full stack trace. |
trace_line |
varchar |
The corresponding line |
The names of the tables and columns can be customized by calling SQLiteAppender#setDbNameResolver()
with a custom implementation of DBNameResolver
. A built-in resolver that adds custom prefixes/suffixes is SimpleDBNameResolver
.
The exact location (e.g., an absolute path including filename) of the SQLite database can be set via SQLiteAppender#setFilename()
. If no name is set, the filename defaults to <context database directory>/logback.db
, obtained from android.content.Context#getDatabasePath()
(e.g., /com.github.tony19.app/data/databases/logback.db
).
By default, logs have no expiration, so logs are persisted until the database file is manually deleted. Setting an expiration with SQLiteAppender#setMaxHistory()
allows log cleanup to be performed to save disk space. The oldest logs (logs that exceed the specified expiry) would be deleted in between log events and at application startup.
<configuration>
<appender name="DB" class="ch.qos.logback.classic.android.SQLiteAppender">
<!-- maxHistory support added in 1.1.1-6. Cleanup logs older than a week. -->
<maxHistory>7 days</maxHistory>
<!-- location of db filename (default is <context database dir>/logback.db) -->
<filename>/path/to/my/sqlite.db</filename>
<!-- customize names of DB/tables/columns -->
<dbNameResolver class="com.foo.MyDBNameResolver" />
<!-- customize log cleanup (default is to delete log entries older than maxHistory) -->
<logCleaner class="com.foo.MyLogCleaner" />
</appender>
<root level="DEBUG" >
<appender-ref ref="DB" />
</root>
</configuration>
Note that AsyncAppender
supports only one child appender.
To properly shutdown the AsyncAppender
(to reclaim the worker thread and flush the logging events), make sure to stop the logger context in Activity#onDestroy()
.
protected void onDestroy() {
super.onDestroy();
// assume SLF4J is bound to logback-classic in the current environment
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
loggerContext.stop();
}
OR add a <shutdownHook/>
in your XML configuration:
<configuration>
<shutdownHook/>
....
</configuration>
Example: Asynchoronous FileAppender
package com.example;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
configureLogbackByString();
org.slf4j.Logger log = LoggerFactory.getLogger(MainActivity.class);
log.info("hello world!!");
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
@Override
protected void onDestroy() {
super.onDestroy();
// Assume SLF4J is bound to logback-classic in the current environment.
// This must be called to properly shutdown AsyncAppender and flush logs
// upon application exit.
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
loggerContext.stop();
}
String LOGBACK_XML =
"<configuration debug='true'>" +
" <property name='LOG_DIR' value='/data/data/com.example/files' />" +
" <appender name='FILE' class='ch.qos.logback.core.FileAppender'>" +
" <file>${LOG_DIR}/log.txt</file>" +
" <encoder>" +
" <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg%n</pattern>" +
" </encoder>" +
" </appender>" +
" <appender name='ASYNC' class='ch.qos.logback.classic.AsyncAppender'>" +
" <appender-ref ref='FILE' />" +
" </appender>" +
" <root level='DEBUG'>" +
" <appender-ref ref='ASYNC' />" +
" </root>" +
"</configuration>";
private void configureLogbackByString() {
// reset the default context (which may already have been initialized)
// since we want to reconfigure it
LoggerContext lc = (LoggerContext)LoggerFactory.getILoggerFactory();
lc.stop();
JoranConfigurator config = new JoranConfigurator();
config.setContext(lc);
InputStream stream = new ByteArrayInputStream(LOGBACK_XML.getBytes());
try {
config.doConfigure(stream);
} catch (JoranException e) {
e.printStackTrace();
}
}
}
Example: Asynchronous FileAppender
package com.example;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import ch.qos.logback.classic.AsyncAppender;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.util.StatusPrinter;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
configureLogbackDirectly();
org.slf4j.Logger log = LoggerFactory.getLogger(MainActivity.class);
log.info("hello world!!");
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
@Override
protected void onDestroy() {
super.onDestroy();
// assume SLF4J is bound to logback-classic in the current environment
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
loggerContext.stop();
}
private void configureLogbackDirectly() {
// reset the default context (which may already have been initialized)
// since we want to reconfigure it
LoggerContext lc = (LoggerContext)LoggerFactory.getILoggerFactory();
lc.stop();
// setup FileAppender
PatternLayoutEncoder encoder1 = new PatternLayoutEncoder();
encoder1.setContext(lc);
encoder1.setPattern("%d{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg%n");
encoder1.start();
FileAppender<ILoggingEvent> fileAppender = new FileAppender<ILoggingEvent>();
fileAppender.setContext(lc);
fileAppender.setName("FILE");
fileAppender.setFile(this.getFileStreamPath("log.txt").getAbsolutePath());
fileAppender.setEncoder(encoder1);
fileAppender.start();
AsyncAppender asyncAppender = new AsyncAppender();
asyncAppender.setContext(lc);
asyncAppender.setName("ASYNC");
// UNCOMMENT TO TWEAK OPTIONAL SETTINGS
// // excluding caller data (used for stack traces) improves appender's performance
// asyncAppender.setIncludeCallerData(false);
// // set threshold to 0 to disable discarding and keep all events
// asyncAppender.setDiscardingThreshold(0);
// asyncAppender.setQueueSize(256);
// NOTE: asyncAppender takes only one child appender
asyncAppender.addAppender(fileAppender);
asyncAppender.start();
// add the newly created appenders to the root logger;
// qualify Logger to disambiguate from org.slf4j.Logger
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
root.addAppender(asyncAppender);
StatusPrinter.print(lc);
}
}