Skip to content
Tony Trinh edited this page Aug 27, 2019 · 26 revisions

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.

Configuration in XML

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>

Configuration in Code

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

Tables

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

Custom Names

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.

Database filename

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).

Log cleanup

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.

Example XML config

<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>

Configuration by in-memory XML string

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();
    }
  }
}

Configuration in Code

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);
  }
}