How to create a circlular file logger with Timber

In some applications, I need to store my logs in a file aside of traditional logcat. For this, I am making use of Timber library. Because I don’t want to make my device full of logs, I wanted to use circular log files so that I can control the maximum amount of bytes taken by log data. To achieve this, I will use java Logger API to implement a new Timber.Tree. I also want some feature like log formatting and filtering.

All of this is implemented by Treessence library.

Log filtering

To implement filtering an interface is defined :

public interface Filter {

/**
* @param priority Log priority.
* @param tag Tag for log.
* @param message Formatted log message.
* @param t Accompanying exceptions.
* @return {@code true} if the log should be skipped, otherwise {@code false}.
* @see timber.log.Timber.Tree#log(int, String, String, Throwable)
*/
boolean skipLog(int priority, String tag, String message, Throwable t);

boolean isLoggable(int priority, String tag);
}

Priority filtering is provided by an implementation of this interface

public class PriorityFilter implements Filter {

private final int minPriority;

public PriorityFilter(int minPriority) {
this.minPriority = minPriority;
}

@Override
public boolean skipLog(int priority, String tag, String message, Throwable t) {
return priority < minPriority;
}

@Override
public boolean isLoggable(int priority, String tag) {
return priority >= minPriority;
}

public int getMinPriority() {
return minPriority;
}
}

We can now create our base class extending Timber.DebugTree

public class PriorityTree extends Timber.DebugTree {

private final PriorityFilter priorityFilter;
private Filter filter = NoFilter.INSTANCE;

/**
* @param priority priority from witch log will be logged
*/
public PriorityTree(int priority) {
this.priorityFilter = new PriorityFilter(priority);
}

/**
* Add additional {@link Filter}
*
* @param f Filter
* @return itself
*/
public PriorityTree withFilter(@NotNull Filter f) {
this.filter = f;
return this;
}

@Override
protected boolean isLoggable(int priority) {
return isLoggable("", priority);
}

@Override
public boolean isLoggable(String tag, int priority) {
return priorityFilter.isLoggable(priority, tag) && filter.isLoggable(priority, tag);
}

public PriorityFilter getPriorityFilter() {
return priorityFilter;
}

public Filter getFilter() {
return filter;
}

/**
* Use the additional filter to determine if this log needs to be skipped
*
* @param priority Log priority
* @param tag Log tag
* @param message Log message
* @param t Log throwable
* @return true if needed to be skipped or false
*/
protected boolean skipLog(int priority, String tag, @NotNull String message, Throwable t) {
return filter.skipLog(priority, tag, message, t);
}
}

This class can filter on two parameters :

  • First parameter is obviously log priority. This is done thanks to PriorityFilter instance.
  • Second parameter is an additional Filter instance that can be provided by caller.

Log formatting

Log formatting is obtained thanks to a Formatter class whose interface is defined as follow

public interface Formatter {

String format(int priority, String tag, String message);
}

Each formatter can display log to a defined format. For instance, logcat format is « MM-dd HH:mm:ss:SSS {priority}/{tag}({thread id}) : {message}\n ». Another format would be « {tag} : {message} »

Because we want to log in a file what we get in logcat, then we need to implement a logcat formatter

public class LogcatFormatter implements Formatter {

public static final LogcatFormatter INSTANCE = new LogcatFormatter();
private static final String SEP = " ";

private final HashMap<Integer, String> prioPrefixes = new HashMap<>();

private LogcatFormatter() {
prioPrefixes.put(Log.VERBOSE, "V/");
prioPrefixes.put(Log.DEBUG, "D/");
prioPrefixes.put(Log.INFO, "I/");
prioPrefixes.put(Log.WARN, "W/");
prioPrefixes.put(Log.ERROR, "E/");
prioPrefixes.put(Log.ASSERT, "WTF/");
}

@Override
public String format(int priority, String tag, @NotNull String message) {
String prio = prioPrefixes.get(priority);
if (prio == null) {
prio = "";
}
return TimeUtils.timestampToDate(System.currentTimeMillis(), "MM-dd HH:mm:ss:SSS")
+ SEP
+ prio
+ (tag == null ? "" : tag)
+ "(" + Thread.currentThread().getId() + ") :"
+ SEP
+ message
+ "\n";
}
}

Priority class can then be extended to add format functionality

/**
* Base class to format logs
*/
public class FormatterPriorityTree extends PriorityTree {
private Formatter formatter = getDefaultFormatter();

public FormatterPriorityTree(int priority) {
super(priority);
}

/**
* Set {
@link Formatter}
*
*
@param f formatter
*
@return itself
*/
public FormatterPriorityTree withFormatter(Formatter f) {
this.formatter = f;
return this;
}

/**
* Use its formatter to format log
*
*
@param priority Priority
*
@param tag Tag
*
@param message Message
*
@return Formatted log
*/
protected String format(int priority, String tag, @NotNull String message) {
return formatter.format(priority, tag, message);
}

/**
*
@return Default log {@link Formatter}
*/
protected Formatter getDefaultFormatter() {
return NoTagFormatter.INSTANCE;
}

@Override
protected void log(int priority, String tag, @NotNull String message, Throwable t) {
super.log(priority, tag, format(priority, tag, message), t);
}
}

File logging

We have seen how to filter and format logs. We can now start logging in file.

For this we need a java.util.logging.Logger instance. It will be used in conjunction with java.util.logging.FileHandler that do actual file logging. We will see how to create a Logger instance later.

public class FileLoggerTree extends FormatterPriorityTree {
private final Logger logger;

private FileLoggerTree(int priority,
Logger logger) {
super(priority);
this.logger = logger;
}
}

To activate logcat formatting by default, getDefaultFormatter() method is overridden


@Override
protected fr.bipi.tressence.common.formatter.Formatter getDefaultFormatter() {
return LogcatFormatter.INSTANCE;
}

We need to convert logcat level to java.util.logging.Level

private Level fromPriorityToLevel(int priority) {
switch (priority) {
case Log.VERBOSE:
return Level.FINER;
case Log.DEBUG:
return Level.FINE;
case Log.INFO:
return Level.INFO;
case Log.WARN:
return Level.WARNING;
case Log.ERROR:
return Level.SEVERE;
case Log.ASSERT:
return Level.SEVERE;
default:
return Level.FINEST;
}
}

Actual logging is done by this method

@Override
protected void log(int priority, String tag, @NotNull String message, Throwable t) {
if (skipLog(priority, tag, message, t)) {
return;
}

logger.log(fromPriorityToLevel(priority), format(priority, tag, message));
if (t != null) {
logger.log(fromPriorityToLevel(priority), "", t);
}
}

It is logging in using java.utils.logging API with log level conversion and logcat formatting

We haven’t seen how to provide the right logger. Let see how to configure it.

A builder class is defined to create a FileLoggerTree instance. This builder contains some default:

public static class Builder {
// 1 mb byte of data
private static final int SIZE_LIMIT = 1048576;
// Max 3 files for circular logging
private static final int NB_FILE_LIMIT = 3;

// Base filename.
// log index will be appended so actual file name will be
// "log.0" or "log.1"
// To parametrize where index is put, "%g" can be placed
// in file name. For instance "log%g.logcat" will give
// "log0.logcat", "log1.logcat" and so on
private String fileName = "log";
// Directory where files are stored
private String dir = "";
// Min priority to log from
private int priority = Log.INFO;
private int sizeLimit = SIZE_LIMIT;
private int fileLimit = NB_FILE_LIMIT;
// append log to already existing log file
private boolean appendToFile = true;

...

java.util.logging.Logger are created and managed by java.util.logging.LogManager. To bypass this a simple static class is used


/**
* Custom logger class that has no references to LogManager
*/
private static class MyLogger extends Logger {

/**
* Constructs a {@code Logger} object with the supplied name and resource
* bundle name; {@code notifyParentHandlers} is set to {@code true}.
* <p/>
* Notice : Loggers use a naming hierarchy. Thus "z.x.y" is a child of "z.x".
*
* @param name the name of this logger, may be {@code null} for anonymous
* loggers.
*/
MyLogger(String name) {
super(name, null);
}

public static Logger getLogger(String name) {
return new MyLogger(name);
}
}

Creation of java.util.logging.Logger can start

public FileLoggerTree build() throws IOException {
// Log file path
String path = FileUtils.combinePath(dir, fileName);
// File handler that is performing file logging
FileHandler fileHandler;
// Our custom logger
Logger logger = MyLogger.getLogger(TAG);
// We force level to ALL because priority filtering is
// done by our Tree implementation
logger.setLevel(Level.ALL);
// File handler can now be created
fileHandler = new FileHandler(path, sizeLimit, fileLimit, appendToFile);
// Formating is done by our Tree implementation
fileHandler.setFormatter(new NoFormatter());
// Configure java Logger
logger.addHandler(fileHandler);
// finally we got here !
return new FileLoggerTree(priority, logger);
}

Full code of FileLoggerTree is here : https://github.com/bastienpaulfr/Treessence/blob/master/treessence/src/main/java/fr/bipi/tressence/file/FileLoggerTree.java

This tree can then be planted like this

FileLoggerTree fileTree = FileLoggerTree.Builder()
.withFileName("log%g.logcat")
.withMinPriority(Log.VERBOSE)
.build()
Timber.plant(fileTree)

Thanks for reading this. Full source code is available here

Publish a Kotlin lib with gradle Kotlin DSL

I wanted to play more with Kotlin and I wanted to publish KGeoGson lib to a remote maven repo.

I was following gradle guide to build my kotlin project with Kotlin DSL.

Bootstrap Kotlin project

Create project directory with files of your library you want to build and publish. You may have a directory structure like the following:

project
├── build.gradle.kts
├── settings.gradle.kts
└── my-kotlin-library
├── build.gradle.kts
└── src
├── main
│   └── kotlin
│   └── org
│   └── example
│   └── MyLibrary.kt
└── test
└── kotlin
└── org
└── example
└── MyLibraryTest.kt

Setup

Root build.gradle.kts

plugins {
`build-scan`
}

buildScan {
termsOfServiceUrl = "https://gradle.com/terms-of-service"
termsOfServiceAgree = "yes"

publishAlways()
}

val clean by tasks.creating(Delete::class) {
delete(rootProject.buildDir)
}

In this file, « build-scan » plugin is activated and a clean task is added. That’s all for it

settings.gradle.kts

include("lib")

In this file, we define our modules

lib/build.gradle.kts

First we are adding some plugins

plugins {
// Add Kotlin plugin to build our Kotlin lib
kotlin("jvm") version "1.3.21"
// Get version from git tags
id("fr.coppernic.versioning") version "3.1.2"
// Documentation for our code
id("org.jetbrains.dokka") version "0.9.17"
// Publication to bintray
id("com.jfrog.bintray") version "1.8.4"
// Maven publication
`maven-publish`
}

Then we are defining dependencies and their repositories for our code

repositories {
jcenter()
mavenCentral()
}

dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7")
implementation("com.google.code.gson:gson:2.8.5")

testCompile("junit:junit:4.12")
}

We need some more tasks to add sources and javadoc to our lib. We are starting by configuring dokka task:

tasks {
dokka {
outputFormat = "html"
outputDirectory = "$buildDir/javadoc"
moduleName = rootProject.name
}
}

We can then bundle documentation into a jar

val dokkaJar by tasks.creating(Jar::class) {
group = JavaBasePlugin.DOCUMENTATION_GROUP
description
= "Assembles Kotlin docs with Dokka"
archiveClassifier.set("javadoc")
from(tasks.dokka)
dependsOn(tasks.dokka)
}

We are creating another jar containing sources

val sourcesJar by tasks.creating(Jar::class) {
archiveClassifier.set("sources")
from(sourceSets.getByName("main").allSource)
}

At this stage, you are able to compile your lib. The most important part of this article begins. Let’s see how publication is working. It is very important to configure pom.xml file of maven artifact in a right manner. Otherwise you will not be able to submit your library into JCenter repo.

Let’s start configuring base of maven publish plugin

val artifactName = "libname"
val artifactGroup = "org.example"

publishing {
publications {
create<MavenPublication>("lib") {
groupId = artifactGroup
artifactId = artifactName
// version is gotten from an external plugin
version = project.versioning.info.display
  // This is the main artifact
from(components["java"])
// We are adding documentation artifact
artifact(dokkaJar)
// And sources
artifact(sourcesJar)
  }
}
}

Now we need to add information about package in pom.xml file. You can edit pom.xml with pom.withXml {code


val pomUrl = "..."
val pomScmUrl = "..."
val pomIssueUrl = "..."
val pomDesc = "..."
val pomScmConnection = ""..."
val pomScmDevConnection = "..."

val githubRepo = "..."
val githubReadme = "..."

val pomLicenseName = "The Apache Software License, Version 2.0"
val pomLicenseUrl = "http://www.apache.org/licenses/LICENSE-2.0.txt"
val pomLicenseDist = "repo"

val pomDeveloperId = "..."
val pomDeveloperName = "..."


publishing {
publications {
create<MavenPublication>("lib") {
[...]

pom.withXml {
asNode().apply {
appendNode("description", pomDesc)
appendNode("name", rootProject.name)
appendNode("url", pomUrl)
appendNode("licenses").appendNode("license").apply {
appendNode("name", pomLicenseName)
appendNode("url", pomLicenseUrl)
appendNode("distribution", pomLicenseDist)
}
appendNode("developers").appendNode("developer").apply {
appendNode("id", pomDeveloperId)
appendNode("name", pomDeveloperName)
}
appendNode("scm").apply {
appendNode("url", pomScmUrl)
appendNode("connection", pomScmConnection)
}
}
}
}
}
}

Now that your maven publication is well configured, you can configure bintray plugin

bintray {
  // Getting bintray user and key from properties file or command line
user = if (project.hasProperty("bintray_user")) project.property("bintray_user") as String else ""
key = if (project.hasProperty("bintray_key")) project.property("bintray_key") as String else ""

// Automatic publication enabled
publish = true

// Set maven publication onto bintray plugin
setPublications("lib")

// Configure package
pkg.apply {
repo = "maven"
name = rootProject.name
setLicenses("Apache-2.0")
setLabels("Gson", "json", "GeoJson", "GPS", "Kotlin")
vcsUrl = pomScmUrl
websiteUrl = pomUrl
issueTrackerUrl = pomIssueUrl
githubRepo = githubRepo
githubReleaseNotesFile
= githubReadme

// Configure version
version.apply {
name = project.versioning.info.display
desc
= pomDesc
released = Date().toString()
vcsTag = project.versioning.info.tag
}
}
}

Here we is ! Happy publication !

Use Dagger Provider with Android Material Stepper

On an Android project, I am using android-material-stepper from StepStone. I am also using dependency injection with dagger.

I was facing the following problem : How to properly inject Fragments without the need to create a DaggerComponent for each of them ?

Providers are here to help. This is how I am doing

class StepFragmentBL @Inject constructor() :
Fragment(), Step, BlView {

@Inject
lateinit var blPresenter: BlPresenter

...
}

This is one of the Fragment I am creating. We can see the @Inject constructor with @Inject lateinit var that shows that this object is injectable by dagger. It is also implementing Step to be used by StepperLayout and implements another interface.

class BlPresenter @Inject constructor

BlPresenter is a simple injectable class

@Singleton
@Component(modules = [(DeliveryModule::class)])
interface DeliveryComponents {

fun inject(deliveryActivity: DeliveryActivity)
}

This is the component injecting my Activity

@Module
class DeliveryModule(private val activity: DeliveryActivity) {

@Named("Activity")
@Provides
internal fun providesContext(): Context {
return activity
}

@Provides
fun providesFragmentManager(): FragmentManager {
return activity.supportFragmentManager
}
}
class DeliveryActivity : AppCompatActivity() {

@Inject
lateinit var deliveryPresenter: DeliveryPresenter

private lateinit var deliveryComponents: DeliveryComponents

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)

di()
ui()
}

private fun di() {
deliveryComponents = DaggerDeliveryComponents.builder()
.deliveryModule(DeliveryModule(this))
.build()
deliveryComponents.inject(this)
}

private fun ui() {
Timber.v(Info.getMethodName())
stepperLayout.adapter = deliveryPresenter.stepperAdapter
    
}
}

We are creating the component into the Activity here. Then, presenter for this activity is also injected

class DeliveryPresenter @Inject constructor() {

@Inject
lateinit var stepperAdapter: PickingStepperAdapter

}

We are arriving to a StepperAdapter instance, also injected

private const val NB_FRAG = 2

class PickingStepperAdapter @Inject constructor(
fm: FragmentManager,
@Named("Activity") context: Context) :
AbstractFragmentStepAdapter(fm, context) {

/**
* Provider for Fragment
*/
@Inject
lateinit var blFragmentProvider: Provider<StepFragmentBL>

/**
* Provider for Fragment
*/
@Inject
lateinit var pdtFragmentProvider: Provider<StepFragmentPdt>

override fun getCount(): Int {
return NB_FRAG
}

override fun createStep(position: Int): Step {
Timber.v("${Info.getMethodName()} $position")

return when (position) {
0 -> blFragmentProvider.get()
else -> pdtFragmentProvider.get()
}
}
}

Here, we are just using

@Inject
lateinit var blFragmentProvider: Provider<StepFragmentBL>

to tell dagger to create the code that will provide an instance of fragment each time blFragmentProvider.get() will be called. Indeed, this is called here

return when (position) {
0 -> blFragmentProvider.get()
else -> pdtFragmentProvider.get()
}

Like this, dagger is injecting fragment for us, and we don’t have to care about objects creation

Android : Créer un fichier keystore (jks) à partir d’une clé (.pk8) et d’un certificat (.pem)

  • Extraire la clé contenue dans le fichier pk8 et la mettre en clair dans un fichier pem
openssl pkcs8 -inform DER -nocrypt -in key.pk8 -out key.pem
  • Regrouper la clé et le certificat dans un fichier p12 temporaire
openssl pkcs12 -export -in certificate.pem -inkey key.pem -out platform.p12 -password pass:android -name AndroidDebugKey
  • Générer le fichier jks à partir du p12 (keytool est un outil du framework android)
keytool -importkeystore -deststorepass android -destkeystore platform.jks -srckeystore platform.p12 -srcstoretype PKCS12 -srcstorepass android
  • Vérifier le fichier
keytool -list -v -keystore platform.jks