Skip to main content

How to create a circlular file logger with Timber

· 7 min read

In some applications, I need to store my logs in a file aside of traditional logcat. For this, I am making use of Timber library. Because I don’t want to make my device full of logs, I wanted to use circular log files so that I can control the maximum amount of bytes taken by log data. To achieve this, I will use java Logger API to implement a new Timber.Tree. I also want some feature like log formatting and filtering.

All of this is implemented by Treessence library.

Log filtering#

To implement filtering an interface is defined :

public interface Filter {
    /**     * @param priority Log priority.     * @param tag      Tag for log.     * @param message  Formatted log message.     * @param t        Accompanying exceptions.     * @return {@code true} if the log should be skipped, otherwise {@code false}.     * @see timber.log.Timber.Tree#log(int, String, String, Throwable)     */    boolean skipLog(int priority, String tag, String message, Throwable t);
    boolean isLoggable(int priority, String tag);}

Priority filtering is provided by an implementation of this interface

public class PriorityFilter implements Filter {
    private final int minPriority;
    public PriorityFilter(int minPriority) {        this.minPriority = minPriority;    }
    @Override    public boolean skipLog(int priority, String tag, String message, Throwable t) {        return priority < minPriority;    }
    @Override    public boolean isLoggable(int priority, String tag) {        return priority >= minPriority;    }
    public int getMinPriority() {        return minPriority;    }}

We can now create our base class extending Timber.DebugTree

public class PriorityTree extends Timber.DebugTree {
    private final PriorityFilter priorityFilter;    private Filter filter = NoFilter.INSTANCE;
    /**     * @param priority priority from witch log will be logged     */    public PriorityTree(int priority) {        this.priorityFilter = new PriorityFilter(priority);    }
    /**     * Add additional {@link Filter}     *     * @param f Filter     * @return itself     */    public PriorityTree withFilter(@NotNull Filter f) {        this.filter = f;        return this;    }
    @Override    protected boolean isLoggable(int priority) {        return isLoggable("", priority);    }
    @Override    public boolean isLoggable(String tag, int priority) {        return priorityFilter.isLoggable(priority, tag) && filter.isLoggable(priority, tag);    }
    public PriorityFilter getPriorityFilter() {        return priorityFilter;    }
    public Filter getFilter() {        return filter;    }
    /**     * Use the additional filter to determine if this log needs to be skipped     *     * @param priority Log priority     * @param tag      Log tag     * @param message  Log message     * @param t        Log throwable     * @return true if needed to be skipped or false     */    protected boolean skipLog(int priority, String tag, @NotNull String message, Throwable t) {        return filter.skipLog(priority, tag, message, t);    }}

This class can filter on two parameters :

  • First parameter is obviously log priority. This is done thanks to PriorityFilter instance.
  • Second parameter is an additional Filter instance that can be provided by caller.

## Log formatting

Log formatting is obtained thanks to a Formatter class whose interface is defined as follow

public interface Formatter {
    String format(int priority, String tag, String message);}

Each formatter can display log to a defined format. For instance, logcat format is "MM-dd HH:mm:ss:SSS {priority}/{tag}({thread id}) : {message}\n". Another format would be "{tag} : {message}".

Because we want to log in a file what we get in logcat, then we need to implement a logcat formatter

public class LogcatFormatter implements Formatter {
    public static final LogcatFormatter INSTANCE = new LogcatFormatter();    private static final String SEP = " ";
    private final HashMap<Integer, String> prioPrefixes = new HashMap<>();
    private LogcatFormatter() {        prioPrefixes.put(Log.VERBOSE, "V/");        prioPrefixes.put(Log.DEBUG, "D/");        prioPrefixes.put(Log.INFO, "I/");        prioPrefixes.put(Log.WARN, "W/");        prioPrefixes.put(Log.ERROR, "E/");        prioPrefixes.put(Log.ASSERT, "WTF/");    }
    @Override    public String format(int priority, String tag, @NotNull String message) {        String prio = prioPrefixes.get(priority);        if (prio == null) {            prio = "";        }        return TimeUtils.timestampToDate(System.currentTimeMillis(), "MM-dd HH:mm:ss:SSS")               + SEP               + prio               + (tag == null ? "" : tag)               + "(" + Thread.currentThread().getId() + ") :"               + SEP               + message               + "\n";    }}

Priority class can then be extended to add format functionality

/** * Base class to format logs */public class FormatterPriorityTree extends PriorityTree {    private Formatter formatter = getDefaultFormatter();
    public FormatterPriorityTree(int priority) {        super(priority);    }
    /**     * Set {@link Formatter}     *     * @param f formatter     * @return itself     */    public FormatterPriorityTree withFormatter(Formatter f) {        this.formatter = f;        return this;    }
    /**     * Use its formatter to format log     *     * @param priority Priority     * @param tag      Tag     * @param message  Message     * @return Formatted log     */    protected String format(int priority, String tag, @NotNull String message) {        return formatter.format(priority, tag, message);    }
    /**     * @return Default log {@link Formatter}     */    protected Formatter getDefaultFormatter() {        return NoTagFormatter.INSTANCE;    }
    @Override    protected void log(int priority, String tag, @NotNull String message, Throwable t) {        super.log(priority, tag, format(priority, tag, message), t);    }}

File logging#

We have seen how to filter and format logs. We can now start logging in file.

For this we need a java.util.logging.Logger instance. It will be used in conjunction with java.util.logging.FileHandler that do actual file logging. We will see how to create a Logger instance later.

public class FileLoggerTree extends FormatterPriorityTree {    private final Logger logger;
    private FileLoggerTree(int priority,                           Logger logger) {        super(priority);        this.logger = logger;    }}

To activate logcat formatting by default, getDefaultFormatter() method is overridden

@Overrideprotected fr.bipi.tressence.common.formatter.Formatter getDefaultFormatter() {    return LogcatFormatter.INSTANCE;}

We need to convert logcat level to java.util.logging.Level

private Level fromPriorityToLevel(int priority) {    switch (priority) {        case Log.VERBOSE:            return Level.FINER;        case Log.DEBUG:            return Level.FINE;        case Log.INFO:            return Level.INFO;        case Log.WARN:            return Level.WARNING;        case Log.ERROR:            return Level.SEVERE;        case Log.ASSERT:            return Level.SEVERE;        default:            return Level.FINEST;    }}

Logging is done by this method

@Overrideprotected void log(int priority, String tag, @NotNull String message, Throwable t) {    if (skipLog(priority, tag, message, t)) {        return;    }
    logger.log(fromPriorityToLevel(priority), format(priority, tag, message));    if (t != null) {        logger.log(fromPriorityToLevel(priority), "", t);    }}

It is logging in using java.utils.logging API with log level conversion and logcat formatting

We haven’t seen how to provide the right logger. Let see how to configure it.

A builder class is defined to create a FileLoggerTree instance. This builder contains some default:

public static class Builder {    // 1 mb byte of data    private static final int SIZE_LIMIT = 1048576;    // Max 3 files for circular logging    private static final int NB_FILE_LIMIT = 3;
    // Base filename.    // log index will be appended so actual file name will be    // "log.0" or "log.1"    // To parametrize where index is put, "%g" can be placed    // in file name. For instance "log%g.logcat" will give    // "log0.logcat", "log1.logcat" and so on    private String fileName = "log";    // Directory where files are stored    private String dir = "";    // Min priority to log from    private int priority = Log.INFO;    private int sizeLimit = SIZE_LIMIT;    private int fileLimit = NB_FILE_LIMIT;    // append log to already existing log file    private boolean appendToFile = true;
...}

java.util.logging.Logger are created and managed by java.util.logging.LogManager. To bypass this a simple static class is used

/** * Custom logger class that has no references to LogManager */private static class MyLogger extends Logger {
    /**     * Constructs a {@code Logger} object with the supplied name and resource     * bundle name; {@code notifyParentHandlers} is set to {@code true}.     * <p/>* Notice : Loggers use a naming hierarchy. Thus "z.x.y" is a child of "z.x".     *     * @param name the name of this logger, may be {@code null} for anonymous     *             loggers.     */    MyLogger(String name) {        super(name, null);    }
    public static Logger getLogger(String name) {        return new MyLogger(name);    }}

Creation of java.util.logging.Logger can start

public FileLoggerTree build() throws IOException {  // Log file path  String path = FileUtils.combinePath(dir, fileName);  // File handler that is performing file logging  FileHandler fileHandler;  // Our custom logger  Logger logger = MyLogger.getLogger(TAG);  // We force level to ALL because priority filtering is  // done by our Tree implementation  logger.setLevel(Level.ALL);  // File handler can now be created  fileHandler = new FileHandler(path, sizeLimit, fileLimit,  appendToFile);  // Formating is done by our Tree implementation  fileHandler.setFormatter(new NoFormatter());  // Configure java Logger  logger.addHandler(fileHandler);  // finally we got here !  return new FileLoggerTree(priority, logger);}

Full code of FileLoggerTree is here

This tree can then be planted like this

FileLoggerTree fileTree = FileLoggerTree.Builder()        .withFileName("log%g.logcat")        .withMinPriority(Log.VERBOSE)        .build()Timber.plant(fileTree)

Thanks for reading this. Full source code is available here