Formatar os logs da aplicação para simplificar o parsing será nosso principal objetivo nesse post, esta é a primeira etapa de um outro objetivo que é centralizar e analisar programaticamente esses logs para identificar eventos e aplicar ações como indexar, arquivar, notificar (por email/group chat) ou simplesmente delegar para outras ferramentas como bug tracker, dashboard de estatísticas (técnicas e/ou de negócio), etc.
O log padrão do Rails é feito em múltiplas linhas, o que facilita a leitura direta durante o desenvolvimento mas dificulta o parsing.
A documentação do Fluentd exemplifica o parsing multiline de um GET padrão do Rails mas basta acontecer qualquer coisa diferente do “normal” (como um warning ou exception com backtrace) que a regex é rapidamente invalidada. Para superar essa dificuldade vamos logar cada request em uma única linha e além disso adicionar informações para contextualizar o conteúdo de acordo com a nossa necessidade.
Existem diversas ferramentas para customizar os logs do Rails, depois de testar algumas escolhi a gem Lograge para nos ajudar neste ponto.
A primeira coisa a fazer é instalar e configurar a gem:
1
|
|
1 2 |
|
Com isso já temos um log bem mais fácil de parsear:
method=GET path=/ format=html controller=site action=index status=200 duration=262.28 view=108.93 db=3.84
Vamos ainda extrair mais algumas informações dos nossos objetos “request“ e “session” criando o módulo CustomLog::ControllerHelper:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Incluiremos esse módulo em nosso ApplicationController:
1 2 3 |
|
Agora temos a possibilidade de extrair as informações que desejamos, faremos isso criando a classe CustomLog::Subscriber:
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
|
Configuramos também a gem “lograge” para usar nossa classe:
1 2 3 |
|
Agora complementamos nosso log com:
time=’2014-06-15 13:13:47 -0300' session_id=abcde12345 host=app01 request_id=a1b2c3d4e5 remote_ip=179.209.205.143 user_agent=’Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:26.0) Gecko/20100101 Firefox/26.0' referer=/things/new flash={:notice=>”Dados gravados com sucesso”} params={“thing”=>{“name”=>”Teste”}
Além disso vamos incluir também as mensagens de validação do objeto que estamos manipulando, para isso vamos alterar o helper e o subscriber:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
complementando o log com coisas do tipo:
flash={:error=>”Não foi possível gravar os dados”} validations={"name":["já está em uso"]}
Em uma aplicação multitenant também é importante ter informações sobre o usuário e o tenant (empresa, conta, etc) com que os requests estão interagindo, para isso alteramos mais uma vez o helper e o subscriber:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Como essas informações são específicas de cada aplicação precisamos customizar o método em nosso controller:
1 2 3 4 5 6 |
|
Dessa forma complementamos nosso log com:
user_id=987 account_id=123
E para finalizar vamos logar o backtrace completo em caso de exceptions, para isso vamos complementar o Instrumenter do ActiveSupport:
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 |
|
e alterar novamente nosso subscriber:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Isso adiciona ao nosso log informações como:
exception_class=RuntimeError exception_message=Test backtrace=’[“complete", "backtrace", "array"]'
Com isso atingimos nosso objetivo que é ter o log em uma única linha contextualizado com todas as informações que nos interessam para fazer análise de forma automatizada.
Por precaução fiz um benchmark bem simples com o antes/depois de nossas customizações e a diferença foi de 0.01 segundo simulando 1000 requests com concorrência de 30, o que acredito ser aceitável para a maioria dos casos.
Veja um Gist com o conteúdo completo de cada arquivo que criamos aqui.
Na próxima etapa vamos usar um agregador para receber esses logs customizados, parsear e tomar decisões sobre onde persistir, arquivar, indexar, notificar, disponibilizar para outros serviços (via PubSub, webhook, etc) totalmente independente/desacoplado da aplicação e tolerante a falhas.
Referências e links relacionados:
- http://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying
- http://gojko.net/2006/12/09/logging-anti-patterns/
- http://blog.mmlac.com/log4r-for-rails/
- http://rubyjunky.com/cleaning-up-rails-4-production-logging.html
- http://www.paperplanes.de/2012/3/14/on-notifications-logsubscribers-and-bringing-sanity-to-rails-logging.html
- http://www.railsonmaui.com//blog/2013/05/08/strategies-for-rails-logging-and-error-handling/
- http://edgeguides.rubyonrails.org/active_support_instrumentation.html