Search code examples
androidtimber-android

Write all exception in file


I'm using Timber to write some logs to file which located on device. For now, I'm writing my selected logs and some response from server using HTTP interceptor. But I want to write in file all exception (fatal, for example). Is it possible with Timber or other library?

For now I'm using Fabric, but my app not always have internet connection with outer world

P.S. I want to write ALL fatal exception without try/catch

TimberLooger.class

public class FileLoggingTree
{
    /**
     * Sends an error message to LogCat and to a log file.
     * @param context The context of the application.
     * @param logMessageTag A tag identifying a group of log messages. Should be a constant in the
     *                      class calling the logger.
     * @param logMessage The message to add to the log.
     */
    public static void e(Context context, String logMessageTag, String logMessage)
    {
        if (!Log.isLoggable(logMessageTag, Log.ERROR))
            return;

        int logResult = Log.e(logMessageTag, logMessage);
        if (logResult > 0)
            logToFile(context, logMessageTag, logMessage);
    }

    /**
     * Sends an error message and the exception to LogCat and to a log file.
     * @param context The context of the application.
     * @param logMessageTag A tag identifying a group of log messages. Should be a constant in the
     *                      class calling the logger.
     * @param logMessage The message to add to the log.
     * @param throwableException An exception to log
     */
    public static void e
    (Context context, String logMessageTag, String logMessage, Throwable throwableException)
    {
        if (!Log.isLoggable(logMessageTag, Log.ERROR))
            return;

        int logResult = Log.e(logMessageTag, logMessage, throwableException);
        if (logResult > 0)
            logToFile(context, logMessageTag,
                    logMessage + "\r\n" + Log.getStackTraceString(throwableException));
    }

// The i and w method for info and warning logs
// should be implemented in the same way as the e method for error logs.

    /**
     * Sends a message to LogCat and to a log file.
     * @param context The context of the application.
     * @param logMessageTag A tag identifying a group of log messages. Should be a constant in the
     *                      class calling the logger.
     * @param logMessage The message to add to the log.
     */
    public static void v(Context context, String logMessageTag, String logMessage)
    {
        // If the build is not debug, do not try to log, the logcat be
        // stripped at compilation.
        if (!BuildConfig.DEBUG || !Log.isLoggable(logMessageTag, Log.VERBOSE))
            return;

        int logResult = Log.v(logMessageTag, logMessage);
        if (logResult > 0)
            logToFile(context, logMessageTag, logMessage);
    }

    /**
     * Sends a message and the exception to LogCat and to a log file.
     * @param logMessageTag A tag identifying a group of log messages. Should be a constant in the
     *                      class calling the logger.
     * @param logMessage The message to add to the log.
     * @param throwableException An exception to log
     */
    public static void v
    (Context context,String logMessageTag, String logMessage, Throwable throwableException)
    {
        // If the build is not debug, do not try to log, the logcat be
        // stripped at compilation.
        if (!BuildConfig.DEBUG || !Log.isLoggable(logMessageTag, Log.VERBOSE))
            return;

        int logResult = Log.v(logMessageTag, logMessage, throwableException);
        if (logResult > 0)
            logToFile(context, logMessageTag,
                    logMessage + "\r\n" + Log.getStackTraceString(throwableException));
    }

// The d method for debug logs should be implemented in the same way as the v method for verbose logs.

    /**
     * Gets a stamp containing the current date and time to write to the log.
     * @return The stamp for the current date and time.
     */
    private static String getDateTimeStamp()
    {
        Date dateNow = Calendar.getInstance().getTime();
        // My locale, so all the log files have the same date and time format
        return (DateFormat.getDateTimeInstance
                (DateFormat.SHORT, DateFormat.SHORT, Locale.CANADA_FRENCH).format(dateNow));
    }

    /**
     * Writes a message to the log file on the device.
     * @param logMessageTag A tag identifying a group of log messages.
     * @param logMessage The message to add to the log.
     */
    private static void logToFile(Context context, String logMessageTag, String logMessage)
    {
        try
        {
            // Gets the log file from the root of the primary storage. If it does
            // not exist, the file is created.
            File logFile = new File(Environment.getRootDirectory(),
                    "TestApplicationLog.txt");
            if (!logFile.exists())
                logFile.createNewFile();
            // Write the message to the log with a timestamp
            BufferedWriter writer = new BufferedWriter(new FileWriter(logFile, true));
            writer.write(String.format("%1s [%2s]:%3s\r\n",
                    getDateTimeStamp(), logMessageTag, logMessage));
            writer.close();
            // Refresh the data so it can seen when the device is plugged in a
            // computer. You may have to unplug and replug to see the latest
            // changes
            MediaScannerConnection.scanFile(context,
                    new String[] { logFile.toString() },
                    null,
                    null);
        }
        catch (IOException e)
        {
            Log.e("com.cindypotvin.Logger", "Unable to log exception to file.");
        }
    }
}

TimberLogger.class

public class TimberLooger extends Timber.DebugTree {
    private static final String TAG = FileLoggingTree.class.getSimpleName();

    private Context context;

    public TimberLooger(Context context) {
        this.context = context;
    }

    @Override
    protected void log(int priority, String tag, String message, Throwable t) {

        try {

            File direct = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/FileLocation");

            if (!direct.exists()) {
                direct.mkdir();
            }

            String fileNameTimeStamp = new SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(new Date());
            String logTimeStamp = new SimpleDateFormat("E MMM dd yyyy 'at' hh:mm:ss:SSS aaa", Locale.getDefault()).format(new Date());

            String fileName = fileNameTimeStamp + ".html";

            File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/FileLocation" + File.separator + fileName);

            file.createNewFile();

            if (file.exists()) {

                OutputStream fileOutputStream = new FileOutputStream(file, true);

                fileOutputStream.write(("<p style=\"background:lightgray;\"><strong style=\"background:lightblue;\">&nbsp&nbsp" + logTimeStamp + " :&nbsp&nbsp</strong>&nbsp&nbsp" + message + "</p>").getBytes());
                fileOutputStream.close();

            }

            //if (context != null)
            //MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, null, null);

        } catch (Exception e) {
            Log.e(TAG, "Error while logging into file : " + e);
        }
    }
}

Solution

  • Yes, it's possible to use Timber to write your exceptions to a file. To achieve that you have to create a custom Timber Tree, like the example below (In Kotlin):

    import timber.log.Timber
    
    class FileLogTree : Timber.Tree() {
    
        override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
    
            if (priority == Log.ERROR) {
                try {
                    val directory = Environment.getExternalStoragePublicDirectory("${Environment.DIRECTORY_DOCUMENTS}/logs")
    
                    if (!directory.exists())
                        directory.mkdirs()
    
                    val fileName = "myLog.txt"
    
                    val file = File("${directory.absolutePath}${File.separator}$fileName")
    
                    file.createNewFile()
    
                    if (file.exists()) {
                        val fos = FileOutputStream(file, true)
    
                        fos.write("$message\n".toByteArray(Charsets.UTF_8))
                        fos.close()
                    }
    
                } catch (e: IOException){
                    Log.println(Log.ERROR,"FileLogTree", "Error while logging into file: $e")
                }
            }
        }
    }
    

    Later, on your Timber initialization process, set this custom tree:

    Timber.plant(FileLogTree())
    

    In this example, Timber will write all "Timber.e()" logs to a file named "myLog.txt" inside your documents/logs folder.

    You'll also need the WRITE_EXTERNAL_STORAGE permission for that custom Tree to work. You can read more about getting this permission on this answer.

    As for the "get all exceptions" part, you'll have to create a custom Uncaught Exception Handler and change the default Thread.UncaughtExceptionHandler once your application (or activities) starts. To do so, call: Thread.setDefaultUncaughtExceptionHandler(new YourExceptionHandler(context));. Inside that handler you can chose what to do with the exception (Like calling Timber.e()).

    Check this answer to learn more on how to implement that. Alternatively, you can take a look at this link, this guy made one of those handlers to get those crashes. You can use that with your custom Tree to save the details on your log file.