Dominando seus logs com o Timber

Vamos abordar aqui um dos pontos mais básicos do desenvolvimento de aplicações para Android que é a classe Log do Android. Mas por que falar sobre um tema tão básico como esse? Simples, pois além de poder falar sobre boas práticas de tratamento dos seus Logs, é uma ótima oportunidade de apresentar essa ótima biblioteca de abstração da classe Log.

– Tá, mas como o Tinder poderia resolver meus problemas com Log?

Bem, o Tinder pode resolver seu problema de encontro pro fim de semana, mas vou me limitar hoje em como controlar efetivamente o sistema de logs com o Timber. ;)

O problema

O problema é até bem simples. A classe Log é uma classe extremamente útil para que a aplicação jogue dados que sejam úteis para o Logcat sem que você precise por um breakpoint e ir seguindo passo a passo do seu código para observar como ele está se comportando. O seu uso é igualmente simples, bastando inserir uma dessas linhas ao seu código:

//Método Log.e(String tag, String message)
Log.e("Meu Tag", "Oi, eu sou um log de erro!");
Log.i("Meu Tag", "E eu um log de informação");
Log.wtf("Meu Tag", "WHATTAFUC....err What a Terrible Failure");

E assim você envia o seu Log para o Logcat e consegue vê-lo na sua aplicação desta maneira:

Exemplo de saída do Logcat

– Tá mas eu ainda não vi o problema nessa demonstração.

Exato. O problema começa que o Log da sua aplicação não fica visível apenas na janela de Logcat do seu Android Studio. Os logs continuam a aparecer mesmo depois de compilado em apk para release no Google Play, daí qualquer um que plugar o celular em modo de debug pode ver tudo o que sua aplicação joga para o Logcat, e o problema fica ainda maior se você trafega dados sensíveis do usuário para o log, como token de autenticação ou até mesmo usuários, senhas ou número de cartão de crédito.

Este é um erro comum de desenvolvedores iniciantes, mas recentemente um app conhecido, que não irei revelar o nome, expos os logs do cliente http com dados sensíveis como token de autenticação, o que me deu inspiração para abordar sobre este tema e escrever o artigo.

Controlando os seus logs

Há diversas abordagens de como controlar os seus Logs para que eles não caiam em produção. Entre eles:

O método espartano

//Antes
Log.e("CompraActivity", "Número do cartão de crédito: " + creditcard.getNumber());
//Depois
//Log.e("CompraActivity", "Número do cartão de crédito: " + creditcard.getNumber());
THIS IS SPARTA!

As desvantagens aqui são óbvias:

  • Comentar todas as suas linhas que chamam a classe de Log definitivamente não é a solução mais eficiente.
  • Não é à prova de esquecimento. Se esquecer de comentar a linha antes de gerar o apk para produção… bem, já deve saber.

Abstrair a classe Log

Criar uma classe simples que simplesmente apenas jogará para o Logcat caso o app esteja em modo de debug, com uma ajuda do Gradle.

//Farei apenas um método que lida com o Log.e, mas a ideia é a mesma pros demais métodos
public class MyLog {
public static void e(String tag, String message) {
if (BuildConfig.DEBUG) {
Log.e(tag, message);
}
}
}
//Usando a classe
Mylog.e("ActivityTag", "Eita, deu erro.");

As vantagens aqui são:

  • Funciona.
  • À prova de esquecimento, já que provavelmente você não vai gerar um build de debug e por em produção.

Mas há desvantagens:

  • Não funciona sempre (vou abordar melhor sobre isso já já).
  • Para cada método da classe Log, vai virar um método na sua classe, ou terá que criar um método maior que receba o tipo de log, além do tag e mensagem e ainda não resolve o problema citado acima. Ou seja, sua classe pode tender a ficar maior que o desejado e não solucionará o problema totalmente.

– Mas ainda não entendi. Você falou que não funciona sempre, mas pelo o que estou lendo aqui e revendo funciona perfeitamente sim, não vou gerar um apk de debug para por na loja.

Sim, na maioria dos casos esse código soluciona bem o problema com os seus logs, porém não é uma bala de prata e o culpado disso tudo é justamente o BuildConfig.DEBUG. Se seu projeto tem módulos de biblioteca (Android Library), tenho uma má notícia para te dar: Em bibliotecas, o BuildConfig.DEBUG sempre retornará false, logo, para esse caso, seu controle de logs baseado na solução acima se torna inútil. Para resolver este problema, há uma solução alternativa usando o Gradle:

//No build.gradle da sua lib:
buildConfigField 'boolean', 'LOG_ENABLED', 'true'
//No código da sua lib:
if (BuildConfig.LOG_ENABLED) {
Log.e(tag, message)
}

Essa maneira também resolve o problema de controle de logs em bibliotecas, porém voltamos ao problema de risco de esquecimento. Basta deixar o LOG_ENABLED = true no seu build.gradle e publicar o apk que o estrago estará feito.

Usando o Timber para resolver definitivamente o problema

Depois de um monte de leitura sobre logs, enfim chegamos ao que interessa, o Timber. Desculpe se o texto estava longo e até didático demais, mas é interessante abordar essas alternativas para que o problema fosse devidamente explicado. :)

O que é o Timber?

O Timber era uma solução interna para logs utilizada pelo Jake Wharton em seus projetos, até o dia que ele percebeu que a solução era tão boa que valia a pena disponibiliza-la publicamente.

Usar o Timber no seu código é absurdamente simples, bastando chamar os métodos Timber.i, Timber.e, Timber.wtf no lugar do Log.i, Log.e, Log.wtf, por exemplo. Caso você já tenha uma classe de abstração de logs no seu código e ele estiver sendo amplamente utilizado, você pode simplesmente efetuar a mudança diretamente na sua classe de abstração. Ao usar o Timber, veremos algumas vantagens aqui:

Diga adeus para isso:

public static final String TAG = "ActivityTag";
...
Log.e(TAG, "eita, deu erro.");

E dê olá para isso:

Timber.tag("ActivityTag");
...
Timber.e("eita deu erro.");
//ou
Timber.tag("ActivityTag").e("eita deu erro.");

O nome da tag pode ser dado no onCreate() da sua activity ou mesmo no construtor de sua classe Java. Não especificou um tag? Sem problemas, já que o nome da classe é o tag padrão.

– Ok, parece legal. Mas como isso resolve os problemas que você citou antes?

A solução vem do sistema de plantação de árvores que a biblioteca tem (não confunda com a famosa estrutura de dados). E qual a mágica aqui? Simples, uma vez que você plantou uma ou mais árvores, as mesmas ficam disponíveis sempre que você chama Timber.e(), por exemplo, e o mesmo comportamento acontecerá se você chamar Timber.e() dentro de seu projeto library. Simples assim. :D 
Então vamos ao principal e explicar como funciona o processo de plantar uma árvore no Timber:

Vamos plantar árvores!

O Timber tem um método chamado Timber.plant(Tree tree), onde você planta uma classe que estenda a classe Tree, este método deve ser chamado no onCreate() de sua classe de Application. Para facilitar a vida, o Timber já acompanha uma classe DebugTree que faz o básico, imprimir logs no Logcat, e esta deverá ser plantada para que seus logs cheguem até o Logcat. 
Aqui podemos aplicar tudo o que vimos anteriormente. Crie uma classe que estenda a classe Application e nela basta chamar o método Timber.plant, se a aplicação for de debug:

public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (BuildConfig.DEBUG) {
Timber.plant(new DebugTree());
}
}
}

Com isso, caso esteja gerando um apk de debug, será plantada uma árvore para imprimir todas as chamadas feitas a classe do Timber para o Logcat. Ao gerar um apk de release, nenhuma árvore será plantada e nada aparecerá lá.

– Poxa, bem legal mesmo, mas tem mais?
Ah, tem sim!

Fazendo os logs trabalharem por você

Hora de por as árvores para trabalhar

Como disse anteriormente o Timber acompanha uma classe chamada Tree, que pode ser estendida para que você possa implementar árvores que façam mais coisas além de simplesmente jogar log para o logcat. Vamos a um exemplo que combina o uso do Timber com o Crashlytics:

/**
* CrashlyticsReportingTree
* Apenas um exemplo demonstrando o envio de um Throwable para o
* Crashlytics utilizando o Timber.
*/
private static class CrashlyticsReportingTree extends Timber.Tree {
@Override
protected void log(int priority, String tag, String message, Throwable t) {
if (priority == Log.VERBOSE || priority == Log.DEBUG){
return;
}

if (t != null) {
if (priority == Log.ERROR || priority == Log.WARN) {
Crashlytics.logException(t);
}
}
}
}

Com isso, a integração entre o Timber e o Crashlytics funciona de forma bem simples, voltemos ao exemplo anterior onde plantamos uma DebugTree e vamos fazer uma pequena modificação:

public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (BuildConfig.DEBUG) {
Timber.plant(new DebugTree());
} else {
Timber.plant(new CrashlyticsReportingTree());
}
}
}

Com isso o app release vai passar a tratar as entradas do Timber.w() e Timber.e() para serem enviadas diretamente como erros tratados para o Crashlytics, caso o log esteja acompanhado de qualquer coisa que estenda Throwable. Por exemplo:

try {
whateverMethod(whateverValue);
} catch (WhateverException ex) {
Timber.e(ex, "Oh crap.");
}

Com isso você pode criar os mais diversos tipos de árvore, como arvore que jogue alguns tipos de erros para o Toast do Android, ou logs para serem escritos em arquivo, ou que jogue como evento no Google Analytics ou para seu próprio sistema de reports e acompanhamento, e planta-las de acordo com o critério que você desejar, seja por BuildConfig, por remote config do Firebase ou configuração de container do Google Tag Manager, etc. Há diversas possibilidades de como você pode tratar os logs da sua aplicação.

E como eu instalo isso??

//No build.gradle do seu app e libraries internas do seu projeto
compile 'com.jakewharton.timber:timber:4.5.1'

Obs: Acompanhe sempre no GitHub do projeto para saber a versão mais recente da biblioteca.

Conclusão

Apesar do texto ter sido bem extenso, espero que, com esse artigo, tenha conseguido demostrar algumas boas práticas e ideias para o tratamento de logs, fora apresentar esta biblioteca que também me auxiliou muito, já que resolveu alguns problemas meus e creio que possa solucionar o problema de mais pessoas.

Bonus

Utilizando o Timber como o sistema de log do HttpLoggingInterceptor (OkHttp3 e Retrofit2)

HttpLoggingInterceptor logging = new HttpLoggingInterceptor(new Logger() {
@Override public void log(String message) {
Timber.tag("OkHttp").d(message);
}
});

Links

E mais uns Links para o pessoal
Show your support

Clapping shows how much you appreciated Thiago Passos’s story.