Logo Icon Logo
A Crowd-sourced Cookbook on Writing Great Android® Apps
GitHub logo Twitter logo OReilly Book Cover Art

Using a Local Runtime Application Log for Analysis of Field Errors or Situations

Published? true
FormatLanguage: WikiFormat

Problem:

Users reported something about your App that you don't think should happen, but now that the 'release mode' App is on the market, you have no way to find out whats going on in the users environment and bug reports end up in a 'cannot reproduce' scenario.

Solution:

You need to design a built-in mechanism into your App that will give additional insight in such cases. You know the important events or state changes or the resource needs of your app and if you log them in a run-time application log from your App, then the log becomes an additional much needed resource that goes to the heart of the issue being reported and investigated. This simple preventive measure and mechanism goes a long way in reducing the low user ratings for the App, caused by unforeseen situations, and improves the quality of the overall user experience of the App for all users.

One solution is to use the standard java.util.logging package.

Discussion:

You think you have designed, developed and tested your application and released it on the Android marketplace, and that you can now take a vacation. Not so fast! Apart from the simplistic cases, one cannot take care of all possible scenarios during App testing, not that there is the luxury of time for this, and some unexpected App behavior is bound to be reported by Users some time or the other. It need not necessarily be a bug, it might simply be a run-time situation you haven't encountered in your testing. Prepare for this in advance by designing a runtime application log mechanism into your App.

Log the most important events from your app into the log. For example, a state change, a resource timeout (net access, thread wait), or a maxed out retry count. It might even be worthwhile to defensively log an unexpected code path execution in a strange scenario, or some of the most important notifications that are reached to the user.

Care needs to be taken that these log statements are kept to a minimum necessary number. Else the large size of the log itself may become a problem and, while Log.d() calls are ignored at runtime in signed apps, too many log statements may still slow down the App. Only log the statements that will give insight into how the App is working should be there. Nothing more.

Why won't LogCat and ACRA suffice ?

  • The standard LogCat mechanism isn't useful in end user run-time scenarios since the user is unlikely to have the ability to attach a debugger to his device. Too many Log.d and Log.i statements in your code may negatively impact performance of the App. In fact, for this reason, one shouldn't have Log.* statements compiled into the released App.
  • ACRA works well when the device is connected to the internet. This may not always be true, and some class of applications may not require the internet at all except for ACRA. Also, the stack trace given by ACRA provides instantaneous details of the error, while this recipe provides an 'always on' mechanism that gives locally available perspective over the period of time the App's running.

Here's how the class looks:

// Use these built in mechanisms 
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;


public class RuntimeLog {
  public static final int MODE_DEBUG = 1;
  public static final int MODE_RELEASE = 2;
  public static final int ERROR = 3;
  public static final int WARNING = 4;
  public static final int INFO = 5;
  public static final int DEBUG = 6;
  public static final int VERBOSE = 7;

  // Change this to MODE_DEBUG to use for in-house debugging
  static boolean Mode = MODE_RELEASE;
  static logfileName = "/sdcard/YourAppName.log"
  static Logger logger;
  static LogRecord record;

  //initiate the log on first use of the class and 
  //create your custom formatter 

  static {
    try {
      FileHandler fh = new FileHandler(logfileName, true);
      fh.setFormatter(new Formatter() {
        public String format(LogRecord rec) {
          StringBuffer buf = new StringBuffer(1000);
          buf.append(new java.util.Date().getDate());
          buf.append('/');
          buf.append(new java.util.Date().getMonth());
          buf.append('/');
          buf.append((new java.util.Date().getYear())%100);
          buf.append(' ');
          buf.append(new java.util.Date().getHours());
          buf.append(':');
          buf.append(new java.util.Date().getMinutes());
          buf.append(':');
          buf.append(new java.util.Date().getSeconds());
          buf.append('\n');
          return buf.toString();
        }
      });
      logger = Logger.getLogger(logfileName);
      logger.addHandler(fh);
    }
    catch (IOException e) {
      e.printStackTrace();
    }
  }

  // the log method
  public static void log(int logLevel,String msg) {
    //don't log DEBUG and VERBOSE statements in release mode 
    if (Mode == MODE_RELEASE) && (logLevel >= DEBUG))
      return;  
    record=new LogRecord(Level.ALL, msg);
    record.setLoggerName(logfileName);
    try {
      switch(logLevel) {
        case ERROR: 
          record.setLevel(Level.SEVERE);
          logger.log(record);
          break;
        case WARNING:
          record.setLevel(Level.WARNING);
          logger.log(record);
          break;
        case INFO:
          record.setLevel(Level.INFO);
          logger.log(record);
          break;
        //FINE and FINEST levels may not work on some API versions
        //use INFO instead
        case DEBUG:
          record.setLevel(Level.INFO);
          logger.log(record);
          break;
        case VERBOSE:
          record.setLevel(Level.INFO);
          logger.log(record);
          break;
      }
    }
    catch(Exception exception) {
      exception.printStackTrace();
    }
  }
}

Additional Possibilities

  • The same mechanism can be used to uncover complex run-time issues while the development of the App is in progress too. Thats what the 'Mode' variable is for, set it to MODE_DEBUG.
  • For a complex App with many modules, it might be useful to add the Module name to the log call, as an additional parameter.
  • ClassName and MethodName can be extracted from the LogRecord and added to the log statements too, however, it is not recommended that this be done for run-time logs.

Usage is simple

RuntimeLog.log (RuntimeLog.ERROR, "Network resource access request failed");
RuntimeLog.log (RuntimeLog.WARNING, "App changed state to STRANGE_STATE");

and so on.

Whenever needed, you can always ask for user co-operation to retrieve the log file(s) from SDCARD and send them to your support team. Even better, you could write code to do that at the press of a button!

More things to consider

  • You may decide on multiple strategies to use this mechanism, it needn't be ON always. You can log based on a user preference and enable it only when real end users are trying to reproduce certain scenarios.
  • If it is always on, use a file name with the current date (determined on application start-up) for the log, and delete previous log files older than a certain date deemed no longer useful. This will help keep log file sizes in check.

Conclusion

Designing this preventive, local, run-time log mechanism into your App will leave that window of opportunity open for getting insight into unforeseen run-time issues that your user runs into with your App.

See Also:

ACRA [1]. Getting Bug Reports from Users Automatically with BugSense. Debugging using Log.d and LogCat.