SLF4J und Logback in Android

Wenn man als Java-Entwickler in die Android-Entwicklung einsteigt, ist zunächst vieles aus Java bekannt. Einige Aspekte sind aber auch anders gelöst. So zum Beispiel das Thema Logging. Hier bringt Android seine eigene API mit, die ohne weitere Konfiguration bequem verwendet werden kann. Bei fortgeschrittenen Herausforderungen zeigt dieser Ansatz jedoch seine Grenzen. Hier kann auf vorhandenes Wissen und Bibliotheken aus der Java-Welt zurückgegriffen werden.

Der Original-Artikel mit dem Titel „Android Logging für Java-Profis – SLF4J und Logback in Android“ wurde im „Javamagazin“ veröffentlicht und steht hier als PDF zum Download bereit.

Für Neulinge erfreulich, können in Android ohne weitere Konfiguration die statischen Methoden der Klasse android.util.Log aufgerufen werden, was zu einem Eintrag im systemweiten Log führt. Dieses kann mittels der Anwendung Logcat über den Android Debug Server (DDMS) oder direkt über eine Android Debug Bridge (ADB) Shell eingesehen werden.

Während android.util.Log grundlegende Logging Fiktionalität bequem bereitstellt, bietet es keine Lösungen für fortgeschrittene Herausforderungen wie beispielsweise Konfiguration, Integration der Logs von Bibliotheken, Zugriff auf Logs aus der Anwendung, etc.

Viele Java-Entwickler würden solchen Herausforderungen mittels SLF4J und Logback meistern. Worum es sich bei diesen Frameworks handelt, und wie diese auch in Android eingesetzt werden können, wird im Folgenden gezeigt.

SLF4J und Logback generell

Gerade durch seine einfache API und die Möglichkeit der einheitlichen Konfiguration aller Log Statements, auch von 3rd Party Code, hat sich die Simple Logging Facade For Java (SLF4J) in Java zur de-facto Standard Logging API entwickelt [siehe Weiss, Tal: „We Analyzed 30,000 GitHub Projects – Here Are The Top 100 Libraries in Java, JS and Ruby“].

SLF4J stellt eine schmale API bereit, gegen die man im Code Log Statements absetzt. Zusätzlich existieren Bridges, um Log Statements, die mittels anderer Logging Frameworks abgesetzt wurden, auf SLF4J umzuleiten. Welches Logging Framework, also welche Implementierung der SLF4J API, verwendet wird, muss erst beim Deployment entschieden werden.

Abbildung 1 zeigt die Schichten, Module und deren Zusammenhänge im Kontext von SLF4J. Die einzelnen Module stehen als separate JAR-Dateien zur Verfügung. Die Bezeichnungen der Schichten sind aus der SLF4J Dokumentation übernommen [siehe Legacy APIs]. Vorsicht: Gegensatz dazu stehen die Bezeichnungen, die Log4J Version 2.0 seinen Modulen gibt: Das Bridging Modul (log4j-to-slf4j) heißt hier „Adapter“ und das Modul aus dem Adpation Layer (log4j-slf4j-impl) „Binding“ oder „Bridge“ [siehe log4j2 Dependencies].

Unter anderem existieren Bridging-Module für

  • Log4j 1.x (log4j-over-slf4j),
  • Log4j 2.x (log4j-to-slf4j),
  • Jakarta Commons Logging (jcl-over-slf4j) und
  • Java Util Logging (jul-to-slf4j).

Als eigentliche Implementierung der SLF4J API stehen viele Logging Frameworks zur Auswahl, wie

  • SLF4Js native Implementierungen Logback,
  • dessen Fork logback-android,
  • SLF4J Android aber auch Adapter-Module für
  • Log4j 1.x (slf4j-log4j12),
  • Log4j 2.x (log4j-slf4j-impl),
  • Java Util Logging (slf4j-jdk14),
  • Jakarta Commons Logging (slf4j-jcl) und weitere.

Die Abstraktion über die SLF4J API erlaubt es, das eigentliche Logging Framework leicht auszutauschen, ohne den Code anzupassen.

Durch das Umleiten aller Log Statements auf die einheitliche SLF4J API ist es möglich, das Logging aller eingesetzten Bibliotheken durch das Framework der Wahl einheitlich zu konfigurieren.

Die Konfiguration ist nicht Teil von SLF4J, sondern wird durch das Logging Framework realisiert.

Im Falle von Logback und logback-android ordnet man den im Code definierten Loggern Datensenken zu, so genannte Appender, beispielsweise Dateien, Standard Out oder das systemweite Android Log. Die Formatierung der Log Statements erfolgt innerhalb der Appender. Eine Filterung, welche Log Level ausgegeben werden, kann pro Appender und/oder den im Code definierten Loggern vorgenommen werden.

Abb. 1: SLF4J Kontrollfluss, Module und Schichten

SLF4J in Android

Die Vorzüge von SLF4J können auch mittels Android genutzt werden. Folgende Möglichkeiten bieten sich hier:

  • Java Util Logging (JUL) ist seit Java 1.4 Teil des Java Development Kit und ist auch im Android Software Development Kit (SDK) vorhanden.
  • SLF4J bietet eine leichtgewichtige Implementierung der eigenen API an (slf4j-android, siehe Abbildung 1), die alle Statements einfach an das Android Logging weiterleitet [siehe slf4j-android].
  • Es existiert eine auf Android angepasste Implementierung von Log4j 1.x (android-logging-log4), die nicht mehr aktiv weiter entwickelt zu werden scheint [siehe Logging with Log4J in Android].
  • Außerdem gibt es eine auf Android angepasste Implementierung von Logback (logback-android, siehe Abbildung 1) [siehe logback-android].

An dieser Stelle soll die kontrovers geführte Diskussion über Für und Wider von Logback oder Log4j nicht geführt werden. Vor dem Hintergrund, dass Log4j 2.x für Android noch nicht verfügbar ist [siehe „How to use log4j2 with Android (Logcat)?„], ist logback-android als native Implementierung die vielversprechendste Lösung, um Wissen und gewohnte Funktionalitäten zum Thema Logging aus dem Java-Umfeld auch in Android nutzen zu können.

Im Folgenden wird logback-android näher beleuchtet.

logback-android vs. Logback

Im Kern handelt es sich bei logback-android um einen Fork von Logback, aus dem Abhängigkeiten zu Klassen, die im Android SDK nicht verfügbar sind, entfernt wurden (beispielsweise JMS, JMX, JNDI oder Servlets). Dafür stellt logback-android zusätzliche Features für Android bereit, wie LogcatAppender und SQLiteAppender [siehe logback android FAQ]. Die aktuelle Version von logback-android 1.1.1-6 basiert auf Logback Version 1.1.1, was sich immer an den ersten drei Stellen der Versionsnummer erkennen lässt.

Lizenz

Alle Bestandteile sind open source. Die API, SLF4J, steht unter permissiver MIT-Lizenz. logback-android selbst erbt das duale Lizenzmodell von Logback aus Eclipse Public License (EPL, diese wird beispielsweise auch von JUnit verwendet) und GNU Lesser General Public License (LGPL), was einen möglichst unkomplizierten Einsatz ermöglichen soll. Durch die LGPL werden Inkompatibilitäten mit der GPL vermieden. Die EPL erlaubt den Einsatz in Bereichen, in denen Einschränkungen durch die LGPL befürchtet würden [siehe logback-Lizenz].

Hands on

Das grundlegende Aufsetzen in Android verhält sich wie bei Logback auch. Es werden die Abhängigkeiten zur Logging API (SLF4J) und zur Implementierung (in diesem Falle logback-android) benötigt, siehe Listing 1.

compile 'org.slf4j:slf4j-api:1.7.21'
apk('com.github.tony19:logback-android-classic:1.1.1-6') {
    // workaround issue #73
    exclude group: 'com.google.android', module: 'android'
  }

Listing 1: build.gradle

Es ist empfehlenswert logback-android im apk Scope zu deklarieren (vergleichbar mit dem runtime Scope in Maven), wodurch die Klassen bei der Entwicklung nicht zur Verfügung stehen, sondern erst zur Laufzeit. Dies stellt sicher, dass stets gegen die SLF4J API programmiert wird. In Sonderfällen kann davon abgewichen werden, dazu später mehr.

Log Statements können dann im Code, wie gewohnt, gegen die SLF4J API erfolgen. Siehe Listing 2.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class MainActivity {
 private static final Logger LOG = LoggerFactory.getLogger(MainActivity.class);

 public void someMethod() {
  LOG.info("SLF4J info");
 }
}

Listing 2: Absetzen eines Log Statements

Die Konfiguration erfolgt in der von Logback bekannten logback.xml, die bei Android im Verzeichnis assets abgelegt wird. Listing 3 zeigt ein einfaches Beispiel: Hier werden die Log Statements aller Logger, die mit Level DEBUG oder höher abgegeben wurden an das systemweiten Android Log übergeben.

Android speichert neben der eigentlichen Nachricht eines Log Statements die folgenden Metadaten [siehe Logcat]:

  • Package Name der App (konfiguriert in der AndroidManifest.xml der App),
  • Log Level, Synonym Priorität (vorgegeben durch den Aufruf im Code),
  • Tag (durch logback-android konfigurierbar),
  • Prozess-ID (PID) und Thread-ID (TID) sowie
  • Zeitstempel (werden durch das System gesetzt).

In Listing 3 wird der Tag mittels tagEncoder auf den Namen des Loggers (auf zwölf Zeichen gekürzt) gesetzt. Im encoder Tag kann man die eigentliche Nachricht formatieren. Hier bieten Logback und logback-android die Möglichkeit die eigentliche Nachricht mit weiteren Informationen anzureichern [siehe logback-Konfiguration]. Da Android bereits viele Metadaten abspeichert, wird in Listing 3 darauf verzichtet und nur die eigentliche Nachricht übergeben. Auch der bei anderen Appendern notwendige Zeilenumbruch (%n) ist hier nicht notwendig.

<configuration>
  <appender name="logcat" class="ch.qos.logback.classic.android.LogcatAppender">
    <tagEncoder>
      <pattern>%logger{12}</pattern>
    </tagEncoder>
    <encoder>
      <pattern>%msg</pattern>
    </encoder>
  </appender>
  <root level="DEBUG">
    <appender-ref ref="logcat" />
  </root>
</configuration>

Listing 3: logback.xml

Die Konfiguration kann alternativ direkt in der AndroidManifest.xml oder im Code (erfordert Scope compile in build.gradle) erfolgen [siehe logback-android-Manifest].

Der in Listing 2 gezeigte Aufruf, erzeugt mit der Konfiguration aus Listing 3 beispielsweise folgende Ausgabe im Android Log (kopiert aus Android Studio):

01-22 12:23:33.800 24191-24191/info.schnatterer.logbackandroiddemoI/i.s.l.MainActivity: SLF4J info

Diese folgt folgendem Format:

<Zeitstempel> <PID>-<TID>/<Package Name> <Log Level>/<Tag>: <Nachricht>

Mit diesem Konfigurationsmechanismus lassen auch komplexere Anwendungsfälle realisieren, auf die im Folgenden eingegangen wird.

Fortgeschrittene Anwendungen

Dadurch, dass Android-Apps Client-Anwendungen sind, bestehen dort zum Teil andere Anforderungen an das Logging als beispielsweise bei einer JEE-Anwendung, die auf einem Server läuft. Man möchte beispielsweise Logs in der Anwendung anzeigen, diese mittels der Anwendung versenden, das Log Level zur Laufzeit ändern oder Crash Reports versenden, um die Ursache von Fehlern zu erörtern.

Die Umsetzung dieser Anforderungen existiert zwar nicht out-of-the Box, man kann sie aber mit wenig Zusatzaufwand realisieren.

Die Beispielanwendung zu diesem Artikel [siehe logback-android-Demo] erlaubt es auf Knopfdruck Logs gegen SLF4J und JUL abzusetzen, siehe Abbildung 2.

Abb. 2: Darstellung des Logs innerhalb der Anwendung

Die Anwendung zeigt, wie man

  • in der logback.xml das Schreiben auf täglich wechselnde Logfile konfiguriert,
  • Inhalte von Log Datei in einer TextView anzeigt,
  • das Log Level für den root logger und Logcat- und File-Appender zur Laufzeit (per Preference) einstellt,
  • alle Logfile per E-Mail versendet und
  • das aktuelle Logfile in einem externen Editor anzeigt.

Die dabei benötigte Logik, die von logback-android abhängt, ist in der überschaubaren Klasse Logs gekapselt, die in ein eigenes wiederverwendbares Projekt [siehe logback-android-utils] ausgelagert wurde. Da diese Funktionalität nicht von der SLF4J API bereitgestellt wird, muss der Scope der Abhängigkeit zu logback-android in der build.gradle auf compile verändert werden.

An dieser Stelle ist es auch wichtig, sich die Hierarchie der Logger zu vergegenwärtigen: Der root Logger ist das Elternelement aller Logger. Stellt man ihn auf einen größeren Log Level, als den Logcat Appender, hat der root Logger Vorrang. An einem Beispiel:

  • root Log Level: WARN,
  • Logcat Log Level: INFO
  • Ergebnis: Es werden auch bei Logcat nur Statement mit Level ab WARN geloggt.

 

Der einfachste Weg ist hier die gleiche Einstellung für alle Appender zu verwenden und diese über den root Logger zu steuern. Wenn unterschiedliche Level für die Appender verwendet werden sollen, ist es am einfachsten den root Logger auf ALL und die Log Levels pro Appender einzustellen. Ansonsten muss stets darauf geachtet werden, dass der root Logger ein gleich großes oder größeres Level als die Appender hat, um unerwartetes Verhalten zu vermeiden.

Was in diesem Beispiel nicht gezeigt wird, ist die Anbindung an einen Crash Reporting Anbieter. Mittels der Bibliothek ACRA ist dies jedoch relativ einfach möglich. Hier kann man deklarativ das Logfile an einen Crash Report anhängen [siehe ACRA-Logdatei]. Das Backend, an den der Crash Report versendet wird, kann in ACRA konfiguriert werden. Hier stehen selbst gehostete Open Source Lösungen (z.B. Acralyzer) oder kommerzielle Dienste (z.B. Hockeyapp) zur Verfügung [siehe ACRA-Backends].

Zu guter Letzt soll an dieser Stelle noch auf eine Einschränkung von logback-android hingewiesen werden: Es gibt derzeit keine Möglichkeit Log Statements, die direkt auf das Android Log geschrieben werden, auf SLF4J umzuleiten. Dadurch fallen Log Statements, die von Android Fremdbibliotheken oder von Android selbst mit Package Name der App geschrieben werden, nicht unter die Kontrolle von logback-android. Sie werden deshalb weder durch die konfigurierten Log Levels eingeschränkt, noch an die Appenders weitergeleitet.

ProGuard und Lint

Nutzt man ProGuard, um das APK zu optimieren, sollten die proguard-rules.pro entsprechend der Anleitung erweitert werden, um zur Laufzeit noch alle benötigten Klassen zur Verfügung zu haben [siehe ProGuard]. Erfahrungsgemäß ist das Hinzufügen des folgenden zusätzlichen Statements hilfreich:

-dontwarn org.slf4j.**

Aufwändiger wird es bei der Verwendung des Android Linters. Die von Logback übernommene Abhängigkeit zu javax.mail steht im Android SDK nicht zur Verfügung. Hier gibt es mehre Lösungen

  • Wenn man den SMTPAppender nicht verwendet, kann man das logback-android JAR in dem entsprechenden Check des Linters mittels lint.xml ignorieren [siehe logback-android-Demo lint]. Alternativ kann man den Check in der build.gradle komplett deaktivieren. Von Zweiterem ist jedoch abzuraten, da damit auch Funde bei anderen Abhängigkeiten ausgeschlossen werden.
  • Verwendet man den SMTPAppender, fügt man eine Implementierung für javax.mail zu den Abhängigkeiten hinzu. Eine Möglichkeit ist android-mail [siehe logback-android lint Errors].

Alternativen und Größe

In den letzten Jahren wurden viele Logging Frameworks speziell für Android entwickelt, z.B. [siehe Timber] und [siehe Logger]. Dabei handelt es sich um kleine, leichtgewichtige Frameworks, die die Verwendung von android.util.Log komfortabler machen. Die Leichtgewichtigkeit ist ihr Vorteil gegenüber logback-android, sie sind schneller eingebaut und konfiguriert, nur wenige KB groß und haben nur wenige hundert Methoden. Letzteres ist aufgrund der Beschränkung der Dalvik Executable (DEX) Dateien auf 65536 Methoden nicht unerheblich. Zum Vergleich: logback-android und seine Abhängigkeiten haben ca. 4500 Methoden (gemessen mit [siehe dex-method-counts], als Differenz zweier gleicher APKs mit und ohne logback-android v1.1.1-6).

Die Vorteile von logback-android liegen in der umfangreicheren Funktionalität und Konfigurierbarkeit sowie der Wiederverwendung vorhandenen Wissens aus der Java-Welt.

Gemessen an den GitHub Stars sind timber und logger mit mehreren Tausend Stars bekannter als logback-android mit weniger als 500 (Stand 01/2017). Wie repräsentativ diese Zahlen sind ist jedoch fragwürdig, da selbst Logback und SLF4J, obwohl seit Jahren de-facto Standard in der Java-Welt, weniger als Eintausend Stars aufweisen.

Fazit

Dieser Artikel basiert auf den Erfahrungen, die bei der Entwicklung einer App erlangt wurden [siehe nusic]. Hier ist logback-android seit Jahren mit den oben beschriebenen Funktionalitäten erfolgreich im Einsatz und ermöglicht die einheitliche Konfiguration des Log Levels (auch von Fremdbibliotheken) und Logging in Dateien. Dies ermöglicht dann die Anzeige in der App und das Versenden per E-Mail.

Wer die aus Java gewohnten Logging-Funktionalitäten und seine Erfahrungen auch unter Android einsetzen möchte und nicht am DEX-Methodenlimit kratzt, dem ist logback-android zu empfehlen. Durch die Abstraktionsschicht SLF4J wird zudem ein potenzieller späterer Austausch des Logging Frameworks sehr einfach.

Auch für reine Android-Entwickler ohne Java-Historie können die Funktionalitäten von logback-android von Interesse sein.

In jedem Fall macht es trotzdem Sinn, vor der Entscheidung für ein Logging Framework die Alternativen zu evaluieren. Ein besonderer Fokus verdient hier die Integration des Logging Frameworks mit dem verwendeten Crash Reporting Anbieter.

Diesen Beitrag teilen

Johannes Schnatterer
Solution Architect
Mit besonderem Fokus auf Qualität, Open Source Enthusiasmus, einem Hauch von Pedantismus und der Pfadfinderregel im Gepäck versucht er, die IT-Welt jeden Tag ein winziges Bisschen besser zu machen.