Logger factory with SLF4J message formatting, and support for custom logger extensions, without using any external dependencies.
I wanted to have the convenience and performance benefits of the SLF4J logging interface, without the overhead of including the SLF4J dependency (500+ methods), and a third party wrapper (1000+). So I replicated the basic functionality here, in an extremely small dependency (look at the top of the page for sizes/method counts).
Consider the following Log
statement:
Log.d("MyTag", "Value: " + value + ", Old Value: " + oldValue + ", Object: " + object.toString());
Using SLF4J/Bunyan, this could be rewritten as:
log.debug("Value: {}, Old Value: {}, Object: {}", value, oldValue, object);
This results in no String concatenation being necessary until after we have checked whether the log is within the set threshold level, resulting in less Object allocation, faster execution, and lower memory consumption.
Check out this SLF4j documentation for a more thorough explanation.
Have a look at COMPARISON.md for a quick benchmark comparison demonstrating the speed of Bunyan
A short summary is that Bunyan is about 50% faster at runtime and has 4% of the method count when compared to Logback.
Bunyan requires you to add a bunyan.xml
to your root assets
folder, to read configuration details. If this file is
not present, there are default values which will be used.
A basic configuration file that will perform standard logging is as follows:
<bunyan>
<!-- global threshold and tag pattern -->
<global level="INFO" tagPattern="%S"/> <!-- These are the default values -->
<!-- Simple logcat appender, explained below -->
<appender class="me.oriley.bunyan.BunyanLogcatAppender"/>
</bunyan>
There are 6 acceptable values for logging threshold level
:
- ASSERT: Threshold for
android.util.Log.ASSERT
/log.wtf()
- ERROR: Threshold for
android.util.Log.ERROR
/log.error()
- WARN: Threshold for
android.util.Log.WARN
/log.warn()
- INFO: Threshold for
android.util.Log.INFO
/log.info()
- DEBUG: Threshold for
android.util.Log.DEBUG
/log.debug()
- TRACE: Threshold for
android.util.Log.VERBOSE
/log.trace()
Any logs with a lower priority than the value specified will not be passed along to any of the appenders.
There are 6 placeholder values available for tagPattern
:
%n
: The enclosing class simple name, i.e.MyActivity
%N
: The enclosing class name including package, i.e.com.myapp.MyActivity
%m
: The calling method name, i.e.onResume
%T
: The current thread name, i.e.Thread-11
%t
: The same as%T
, but will only include the thread name if not running on the UI/Main thread.%l
: The logging level, represented as a single character, i.e.A
,E
,W
,I
,D
, orV
Note that %m
tags require traversing the stack trace to find method names, which could have an adverse impact on
performance. This is why I would suggest only using them on debug builds, or passing in the method name as part of
the message instead if necessary.
<bunyan>
<!-- global default values and configuration -->
<global level="INFO" tagPattern="%S"/>
<appender class="me.oriley.bunyan.BunyanLogcatAppender"/>
<!-- Class specific thresholds -->
<logger class="my.app.SpamController" level="ERROR" /> <!-- To quieten a particularly noisy class temporarily -->
<logger class="my.app.CurrentActivity" level="TRACE" /> <!-- For additional logging whilst developing -->
</bunyan>
The gradle plugin bunyan-plugin
(more below) will parse this configuration and generate a class file at compile time,
so that at runtime the static initialisation takes < 0ms and uses no InputStream
or getResourceAsStream
methods that
other libraries use and can introduce lag in application startup, as well as bloating memory consumption.
You can override individual configuration options in a bunyan-overrides.xml
file placed in your root assets
folder.
This can be used to change specific values for certain build types or flavors, without needing to copy and paste the
entire base configuration everywhere.
Included are three sample appenders, that you will need to add to your configuration xml if you would like to have them used. The same method applies to any custom appenders you create, provided they have a zero argument constructor.
<bunyan>
...
<appender class="me.oriley.bunyan.BunyanLogcatAppender"/>
</bunyan>
Takes no arguments and appends all logs to Logcat. This should be used unless you have implemented your own appender to handle this.
Note: Requires an extra dependency (listed in the dependency section below)
There are two different Crashlytics appenders. Only include one of them, depending on your desired usage, otherwise all logs will be sent to the Crashlytics SDK twice.
<bunyan>
...
<appender class="me.oriley.bunyan.crashlytics.BunyanCrashlyticsAppender"/>
</bunyan>
Will forward all logs on to the Crashlytics SDK, which will use them when sending in crash reports.
<bunyan>
...
<appender class="me.oriley.bunyan.crashlytics.BunyanCrashlyticsExceptionAppender"/>
</bunyan>
The same as BunyanCrashlyticsAppender
, except all logged exceptions will also be reported to Crashlytics as
Non-Fatals. Use this if you like to keep track of exceptions you are logging.
Remember, no appenders are automatically installed, so by default no logging will be done if you don't add any to your configuration.
You also have the option of adding your own custom appenders for capturing logs to send to your analytics platform of
choice. Simply implement the BunyanAppender
interface and handle log events as required. You will also need to add
an entry for the appender to your configuration xml.
<bunyan>
...
<!-- Where MyAnalyticsAppender is your custom appender with a zero argument constructor -->
<appender class="com.my.app.MyAnalyticsAppender"/>
</bunyan>
If you need to perform initialisation on your appender, you can add it manually from your code. I would suggest doing
it inside your Application
s attachBaseContext
method, so that it can be capturing logs as soon as possible.
protected void attachBaseContext(Context base) {
...
// Where MyComplicatedAppender is your custom appender
Bunyan.addAppender(new MyComplicatedAppender(this, "Needs", "Arguments"));
}
There is also an addAppenders
method that takes a varargs array, so you can add multiple at the same time:
protected void attachBaseContext(Context base) {
...
Bunyan.addAppenders(new Appender1(context), new Appender2(context));
}
You will need to create a field in each class where you wish to use Bunyan, like so:
public class MyClass {
private static final BunyanCoreLogger log = new BunyanCoreLogger(MyClass.class);
}
Once that is done, you can use the logging interface as per usual. A few examples:
public void myMethod(int value) {
log.trace("method called, time: {}", System.currentTimeMillis());
log.debug("current value: {}, new value: {}", mValue, value);
log.info("slf4j logging is much nicer than android.util.Log: {}, how much?: {}", true, Integer.MAX_VALUE);
log.warn("wtf, much logs");
try {
doSomethingThatMightCauseAnException();
} catch (Exception e) {
log.error("error running myMethod with parameter: {}", value, e);
}
}
If you use Project Lombok, there are four extra modules that can allow you to hook into
the automatic log
field generation that is provided. You should only include one, depending on which annotation you
wish to use to create your log fields. Because Lombok will look for specific classes that are available in the relevant
logging framework, make sure to choose one that does not exist in your application, to avoid namespace clashes
|---------------------| ------------------------------------------------------------------|
| Lombok Annotation | Required Dependency |
|---------------------| ------------------------------------------------------------------|
| @Log4j | compile 'com.github.oriley-me.bunyan:bunyan-lombok-log4j:0.4.0' |
| @Log4j2 | compile 'com.github.oriley-me.bunyan:bunyan-lombok-log4j2:0.4.0' |
| @Slf4j | compile 'com.github.oriley-me.bunyan:bunyan-lombok-slf4j:0.4.0' |
| @XSlf4j | compile 'com.github.oriley-me.bunyan:bunyan-lombok-xslf4j:0.4.0' |
|---------------------| ------------------------------------------------------------------|
After including the correct dependency, you can annotate a class to have the the field generated as per usual:
@Slf4j // If you included the -slf4j dependency
public class MyClass {
// log field is automatically generated by the Lombok plugin
}
- Add JitPack.io repo and
bunyan-plugin
dependency to your buildscript:
buildscript {
repositories {
maven { url "https://jitpack.io" }
}
dependencies {
classpath 'com.github.oriley-me.bunyan:bunyan-plugin:0.5.1'
}
}
- Add JitPack.io to your app projects repositories list:
repositories {
maven { url "https://jitpack.io" }
}
- Apply the plugin to your application or library project, and add the module runtime dependency:
apply plugin: 'com.android.application' || apply plugin: 'com.android.library'
apply plugin: 'me.oriley.bunyan-plugin'
...
dependencies {
// Required
compile 'com.github.oriley-me.bunyan:bunyan-core:0.5.1'
// Only necessary if you plan on using a BunyanCrashlyticsAppender/BunyanCrashlyticsExceptionAppender
compile 'com.github.oriley-me.bunyan:bunyan-crashlytics:0.5.1'
// Make sure include any Lombok helper module you require here
}
If you would like to check out the latest development version, please substitute all versions for develop-SNAPSHOT
.
Keep in mind that it is very likely things could break or be unfinished, so stick the official releases if you want
things to be more predictable.