7 Android Logging Tools Tested & Compared

Logging is essential during development. Therefore, the Android SDK provides the public with a default logger. It is easy to use: we can add tags and also separate logs by different levels.

The SDK’s defult logger is not bad and it’s enough for the basics. But hopefully developers think logging should result in far more than just the basics.

Logs can become a security issue, so make sure you remove all logs before releasing a build. You can remove them by creating a custom logger class, this way logging will depend on a boolean flag. Or you can remove all your logging commands from the binary using Proguard. There are easier ways though, so read on.

Developers can use a lot of libraries if they need more functionality for their logging. Jake Wharton is the epitome of creating great and useful libs, evidently, he has built two for logging.

 

Android Logging Tools

Timber 

First of all, here is Timber, an easy-to-use library based on the default Log. You need to instantiate it in the onCreate method of the Application class, and after that you can call its methods anywhere in your app. It automatically tags your logs (you can override it though), it contains String parser methods, and it has embedded lint rules to detect errors. Timber also provides logs for crashes in production builds, with extending the Tree, and adding logs to your crashreporter.

It makes your everyday logging easier, but it has no special extra features, therefore its just a simple logger.

gradle dependency to use: compile ‘com.jakewharton.timber:timber:4.1.0’

1
2
3
4
Timber.plant(new Timber.DebugTree());
Timber.d("Get city filter");
Timber.i("featuredJobs");
Timber.e("data load failed");

Result:

01-28 21:29:29.128 1021-1050/hu….csoport I/ContentManager: featuredJobs
01-28 21:29:29.128 1021-1050/hu….csoport D/AWSManager: Get city filter
01-28 21:29:29.130 1021-1050/hu….csoport E/ContentManager: data load failed

Lint:

using Timber on Android - lint

 

Hugo 

It is another library from Jake, but it is completely different from Timber. Hugo is an annotation based logging library. Its main purpose is to log specified methods, their input parameters, return values, and running times. You just annotate your method with @DebugLog and it gives you everything needed. It works only in debug mode, and it has no effect in the production code.

To apply it in gradle:

1
2
3
4
5
6
7
8
9
10
buildscript {
 repositories {
   mavenCentral()
 }
 dependencies {
   classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1'
 }
}
apply plugin: 'com.android.application'
apply plugin: 'com.jakewharton.hugo'

Sample code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
getUTCOffset(System.currentTimeMillis());
@DebugLog
public int getUTCOffset(long time) {
  TimeZone timezone = TimeZone.getTimeZone("CET");
  try {
      Date date = new Date(time);
      int offset = timezone.getOffset(date.getTime());
      return offset;
  } catch (Exception e) {
      e.printStackTrace();
      return 0;
  }
}

Result:

01-28 22:03:37.480 14010-14010/hu….csoport V/JobsActivity: ⇢ getUTCOffset(time=1454015017485)
01-28 22:03:37.484 14010-14010/hu….csoport V/JobsActivity: ⇠ getUTCOffset [0ms] = 3600000

It is more than a simple logger, its purpose is not that at all. If you want to log info regarding your methods ˙(like execution time & parameters) you have to add quite a few lines of code and some logic in advance. Though Hugo uses only one annotation for them. Unfortunately with the expanding of RxJava and its observables, the logs give far lesser information than we’d prefer.

 

Frodo 

It is based on Hugo, and it tries to fix the above mentioned RxJava problems.

After you set the gradle correctly (0.8.2 has some problems, but 0.8.1 works as expected) you can use the two annotations with which the lib provides you.

The first one is the @RxLogObservabe, you can use it on every method that returns some kind of Observable. If you do not need every little detail, you can give a scope to your logs (EVERYTHING, STREAM, SCHEDULERS, EVENTS and also NOTHING).

The other one, @RxLogSubScriber, can be added to classes extending Subscriber. If you outsource your subscriber into classes, instead of just pasting it in the code, you will be able to log its methods too.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@RxLogSubscriber
public class CustomSubscriber extends Subscriber<Property> {
    @Override
 public void onCompleted() {
        if (!isUnsubscribed()) {
            unsubscribe();
  }
    }
    @Override
 public void onError(Throwable e) {
    }
    @Override
 public void onNext(Property property) {
    }
@RxLogObservable
public Observable<Property> getCycleProperties() {
    return Observable.create((Subscriber<? super Property> subscriber) -> {
        List<Property> properties = new ArrayList<>();
 properties.add(new Property("0", "http://archiv.termuves.hu/files/tartalom/belso/szentesi_mano/lakas_buda/lakas_buda_2.jpg", "Ház", "27,5", "61", "450 E", "III., Miklós utca", "2,5 szobás", "Jó állapotú", new LatLng(47.49751708, 19.04113054), fakeImages));
 properties.add(new Property("1", "http://morningshow.eu/wp-content/uploads/2013/10/ketszintes-lakas.jpg", "Lakás", "127,5", "61", "45 E", "V., Múzeum krt", "3 szobás", "Kiváló állapotú", new LatLng(47.50083686, 19.03984308), fakeImages));
 properties.add(new Property("2", "http://szenmolnar.com/wp-content/uploads/2013/08/budai_lakas01.jpg", "Lakás", "37,5", "61", "13 E", "XIII., Dagály utca", "2,5 szobás", "Közepes állapotú", new LatLng(47.49954666, 19.03495073), fakeImages));
 properties.add(new Property("3", "http://lakbermagazin.hu/images/stories/gallery/cikkek/otlettar/vgzarquitectura/modern_lakas_a_ter_ugyes_felosztasaval_es_meleg_szinekkel_anyagokkal_feluletekkel_02.jpg", "Ház", "217,5", "61", "450 E", "III., Miklós utca", "2,5 szobás", "Jó állapotú", new LatLng(47.49519747, 19.04035807), fakeImages));
 for(Property prop : properties) {
     subscriber.onNext(prop);
 }
        subscriber.onCompleted();
 });
}

Result:

01-29 08:54:33.783 28927-28927/hu....app D/ContentManager: Frodo => [@Observable :: @InClass -> ContentManager :: @Method -> getCycleProperties()]
01-29 08:54:33.787 28927-28954/hu....app D/ContentManager: Frodo => [@Observable#getCycleProperties -> onSubscribe() :: @SubscribeOn -> RxComputationThreadPool-1]
01-29 08:54:33.787 28927-28954/hu....app D/ContentManager: Frodo => [@Observable#getCycleProperties -> onNext() -> hu....app.view.entities.Property@53c976ac]
01-29 08:54:33.787 28927-28954/hu....app D/ContentManager: Frodo => [@Observable#getCycleProperties -> onNext() -> hu....app.view.entities.Property@53c976f8]
01-29 08:54:33.787 28927-28954/hu....app D/ContentManager: Frodo => [@Observable#getCycleProperties -> onNext() -> hu....app.view.entities.Property@53c97724]
01-29 08:54:33.787 28927-28954/hu....app D/ContentManager: Frodo => [@Observable#getCycleProperties -> onNext() -> hu....app.view.entities.Property@53c97750]
01-29 08:54:33.787 28927-28954/hu....app D/ContentManager: Frodo => [@Observable#getCycleProperties -> onCompleted()]
01-29 08:54:33.787 28927-28954/hu....app D/ContentManager: Frodo => [@Observable#getCycleProperties -> onTerminate() :: @Emitted -> 4 elements :: @Time -> 0 ms]
01-29 08:48:22.123 23476-23476/hu....app D/CustomSubscriber: Frodo => [@Subscriber :: CustomSubscriber -> onNext() -> hu....app.view.entities.Property@53567cd0 :: @ObserveOn -> main]
01-29 08:48:22.123 23476-23476/hu....app D/CustomSubscriber: Frodo => [@Subscriber :: CustomSubscriber -> onNext() -> hu....app.view.entities.Property@53567d1c :: @ObserveOn -> main]
01-29 08:48:22.123 23476-23476/hu....app D/CustomSubscriber: Frodo => [@Subscriber :: CustomSubscriber -> onNext() -> hu....app.view.entities.Property@53567d48 :: @ObserveOn -> main]
01-29 08:48:22.123 23476-23476/hu....app D/CustomSubscriber: Frodo => [@Subscriber :: CustomSubscriber -> onNext() -> hu....app.view.entities.Property@53567d74 :: @ObserveOn -> main]
01-29 08:48:22.123 23476-23476/hu....app D/CustomSubscriber: Frodo => [@Subscriber :: CustomSubscriber -> onCompleted() :: @Received -> 4 elements :: @Time -> 0 ms]
01-29 08:48:22.123 23476-23476/hu....app D/CustomSubscriber: Frodo => [@Subscriber :: CustomSubscriber -> unSubscribe()]
01-29 08:48:22.123 23476-23476/hu....app D/ContentManager: Frodo => [@Observable#getProperties -> onUnsubscribe() :: @ObserveOn -> main]

Frodo has some meaningful skills. Understanding RXJava is not easy, especially for the first time, and debugging doesn’t always help you as you’d expect. Logs are the only real help you can rely on, with Frodo it all happens in one annotation. If your logs return objects like in my example, you just have to override their toString() method to print them in a readable way.

 

XLog

It works similar to Hugo, but it does not perform bytecode weaving, as Hugo does. Therefore it can be used with other libs using this technique. Another extra feature is that you can annotate the whole class, not just methods, and you can also log inaccessible methods, such as TextView.setText(). I tried their examples, but was unable to get any log.

1
2
3
4
5
6
7
8
9
10
11
12
public class TestApplication extends Application {
  @Override
  public void onCreate() {
      super.onCreate();
      List<XLogMethod> xLogMethods = new ArrayList<>();
      xLogMethods.add(new XLogMethod(TextView.class, "setText"));
      XLogConfig.config(XLogConfig.newConfigBuilder(this).logMethods(xLogMethods).timeThreshold(1)
              .build());
  }
}

 

LogLifeCycle

It is a bit of an oldie, but if you need to know the exact methods calling an activity, fragment or view in, you can use LogLifeCycle by annotating the class.

Just add the projects gradle file:

classpath ‘com.github.stephanenicolas.loglifecycle:loglifecycle-plugin:1.0.3’

And annotate the activity you want to track with:

@LogLifeCycle

Result of app start:

02-03 23:01:57.016 10099-10099/? D/LogLifeCycle: com.szoszi.logtest.MainActivity [135741418] ⟳ onApplyThemeResource
02-03 23:01:57.022 10099-10099/? D/LogLifeCycle: com.szoszi.logtest.MainActivity [135741418] ⟳ onWindowAttributesChanged
02-03 23:01:57.027 10099-10099/? D/LogLifeCycle: com.szoszi.logtest.MainActivity [135741418] ⟳ onCreate
02-03 23:01:57.054 10099-10099/? D/LogLifeCycle: com.szoszi.logtest.MainActivity [135741418] ⟳ onSupportContentChanged
02-03 23:01:57.054 10099-10099/? D/LogLifeCycle: com.szoszi.logtest.MainActivity [135741418] ⟳ onContentChanged
02-03 23:01:57.059 10099-10099/? D/LogLifeCycle: com.szoszi.logtest.MainActivity [135741418] ⟳ onStart
02-03 23:01:57.059 10099-10099/? D/LogLifeCycle: com.szoszi.logtest.MainActivity [135741418] ⟳ onTitleChanged
02-03 23:01:57.059 10099-10099/? D/LogLifeCycle: com.szoszi.logtest.MainActivity [135741418] ⟳ onPostCreate
02-03 23:01:57.059 10099-10099/? D/LogLifeCycle: com.szoszi.logtest.MainActivity [135741418] ⟳ onStateNotSaved
02-03 23:01:57.059 10099-10099/? D/LogLifeCycle: com.szoszi.logtest.MainActivity [135741418] ⟳ onResume
02-03 23:01:57.059 10099-10099/? D/LogLifeCycle: com.szoszi.logtest.MainActivity [135741418] ⟳ onResumeFragments
02-03 23:01:57.059 10099-10099/? D/LogLifeCycle: com.szoszi.logtest.MainActivity [135741418] ⟳ onPostResume
02-03 23:01:57.082 10099-10099/? D/LogLifeCycle: com.szoszi.logtest.MainActivity [135741418] ⟳ onAttachedToWindow
02-03 23:01:57.239 10099-10099/? D/LogLifeCycle: com.szoszi.logtest.MainActivity [135741418] ⟳ onWindowFocusChanged
02-03 23:01:57.731 10099-10099/? D/LogLifeCycle: com.szoszi.logtest.MainActivity [135741418] ⟳ onEnterAnimationComplete

It can be quite handy in specific situations, and for learning purposes, but in other times it is absolutely useless.

 

DebugOverlay

It can give you an overlay of the app with the logs you added. You can also close the overlay with its x button.

Add to the gradle dependencies:

debugCompile(‘com.hannesdorfmann:debugoverlay:0.2.1’)

And to manifest:

<uses-permission android:name=”android.permission.SYSTEM_ALERT_WINDOW”/>

using DeBug Overlay on Android

This can be useful if you test it on an individual device.

It has a little bug with orientation change, but can give technical testers a lot of information and feedback.

 

Lynx

Lynx is similar to DebugOverlay, but it is not an overlay. You can add a view to your layout to show logs, or you can shake your device and start the logger activity – you can even filter or share the logs here.

Add this to your gradle dependencies:

compile ‘com.github.pedrovgs:lynx:1.6’
And add this to your manifest:

<activity android:name=”com.github.pedrovgs.lynx.LynxActivity”/>

Here is an example from the LifeCycle logger logs.

using Lynx on Android

With these modifications, you can launch a LynxActivity anywhere, and anytime you want. The shake detector didn’t work for me. It is a really interesting approach of logging, but it isn’t a must have solution.

 

Verdict:

Previously we used only Timber for logging at Wanari. After this little research though, we will combine it with Frodo, because of its RXAndroid support. All the others will continue to have the opportunity, but only for specific problems.
Lynx and DebugOverlay provide logs on testing devices. Most of the time we debug on the Genymotion emulator, Android Studio shows the logs in a far more readable way then.
I hope this summary helped you choose the best logging solution, too!
Gergely Szőke

Gergely Szőke

Having a baby means, in order to keep up, you have far less time for sleeping, but it's definitely worth it.