Logging em scripts
Nesta aula, você aprenderá a implementar logging eficaz em scripts Bash, cobrindo funções de log, níveis de severidade, redirecionamento para arquivo e stderr, e inclusão de timestamps. O conteúdo inclui exemplos práticos, boas práticas e exercícios com respostas.
O logging é uma prática essencial em scripts de shell, pois permite registrar eventos, erros e informações úteis durante a execução. Sem um bom sistema de log, diagnosticar problemas em scripts complexos torna-se uma tarefa árdua, especialmente em ambientes de produção. Nesta aula, vamos explorar técnicas para criar logs robustos em Bash, desde funções simples até a implementação de níveis de severidade e timestamps.
Vamos começar entendendo por que o logging é importante e como podemos estruturá-lo de forma modular, facilitando a manutenção e a reutilização. Em seguida, abordaremos cada subtópico com exemplos práticos que você pode adaptar aos seus próprios scripts.
Funções de log
Uma função de log é um bloco de código que encapsula a lógica de registro de mensagens. Em vez de repetir comandos echo ou printf em todo o script, criamos uma função que centraliza a formatação e o destino das mensagens. Isso torna o código mais limpo, consistente e fácil de alterar.
Vamos criar uma função simples que imprime uma mensagem com um prefixo indicando o nível ou o tipo de log. Por exemplo:
#!/bin/bash
log() {
echo "[LOG] $1"
}
log "Iniciando o script"
log "Processando arquivos..."
A função acima é básica, mas podemos evoluí-la para aceitar múltiplos argumentos e incluir timestamps. Em scripts mais robustos, a função pode ser responsável por escrever em um arquivo, enviar para stderr, ou até mesmo enviar para um serviço de logging externo.
Uma boa prática é definir a função de log no início do script, para que possa ser usada em qualquer parte. Também é comum ter funções separadas para diferentes níveis, como info, warn e error, que chamam uma função interna comum.
Níveis
Níveis de log indicam a severidade ou a importância de uma mensagem. Os níveis mais comuns são: DEBUG, INFO, WARN, ERROR e FATAL. Usar níveis permite filtrar mensagens conforme a necessidade, por exemplo, em produção podemos querer apenas erros, enquanto em desenvolvimento queremos tudo.
Em Bash, podemos implementar níveis usando uma variável que define o nível mínimo a ser exibido. Vamos criar uma função que aceita o nível como primeiro argumento e a mensagem como segundo:
#!/bin/bash
LOG_LEVEL=INFO # Nível mínimo para exibir
log() {
local level=$1
shift
local message="$*"
local levels="DEBUG INFO WARN ERROR FATAL"
local current_level_index=$(echo $levels | tr ' ' '\n' | grep -n $LOG_LEVEL | cut -d: -f1)
local message_level_index=$(echo $levels | tr ' ' '\n' | grep -n $level | cut -d: -f1)
if [ $message_level_index -ge $current_level_index ]; then
echo "[$level] $message"
fi
}
log INFO "Iniciando"
log DEBUG "Detalhes de depuração"
log ERROR "Algo deu errado"
Esse código compara os índices dos níveis para decidir se a mensagem deve ser exibida. Uma abordagem mais simples é usar um valor numérico para cada nível. Por exemplo:
#!/bin/bash
LEVEL_DEBUG=0
LEVEL_INFO=1
LEVEL_WARN=2
LEVEL_ERROR=3
LEVEL_FATAL=4
CURRENT_LEVEL=$LEVEL_INFO
log() {
local level=$1
shift
local message="$*"
if [ $level -ge $CURRENT_LEVEL ]; then
echo "[$level] $message"
fi
}
Nesse caso, usamos números para comparar diretamente. Essa abordagem é mais eficiente e clara. Podemos também criar funções wrapper para cada nível, como info(), debug(), error(), que chamam log com o número correspondente.
Saída para arquivo e stderr
É comum querer salvar logs em um arquivo para análise posterior, e também enviar erros para stderr para que possam ser capturados pelo terminal ou por outros processos. Em Bash, podemos redirecionar a saída de uma função ou de um bloco para um arquivo usando >> (append) ou > (sobrescrever).
Vamos modificar nossa função de log para aceitar um parâmetro que indique o destino. Por exemplo, podemos ter uma variável global LOG_FILE que, se definida, faz com que as mensagens sejam gravadas no arquivo. Para stderr, usamos >&2 no redirecionamento.
#!/bin/bash
LOG_FILE="script.log"
log() {
local level=$1
shift
local message="$*"
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
local formatted="[$timestamp] [$level] $message"
if [ -n "$LOG_FILE" ]; then
echo "$formatted" >> "$LOG_FILE"
fi
if [ $level -ge $LEVEL_ERROR ]; then
echo "$formatted" >&2
else
echo "$formatted"
fi
}
Nesse exemplo, mensagens de erro (nível maior ou igual a ERROR) são enviadas para stderr, enquanto as demais vão para stdout. Se LOG_FILE estiver definido, todas as mensagens também são gravadas no arquivo. Isso permite que o script seja usado em pipelines sem poluir a saída padrão com logs, a menos que desejado.
Outra técnica é usar exec para redirecionar toda a saída do script para um arquivo, mas isso é menos flexível. A abordagem com função é mais controlável.
Timestamps
Timestamps são fundamentais em logs para saber quando cada evento ocorreu. Em Bash, podemos obter a data e hora atual usando o comando date com formatos personalizados. Por exemplo, date +"%Y-%m-%d %H:%M:%S" produz algo como 2025-03-01 14:30:45.
Para incluir timestamps em nossos logs, basta adicionar a chamada ao date dentro da função de log, como fizemos no exemplo anterior. Podemos também usar o formato Unix timestamp (segundos desde 1970) com %s, que é útil para cálculos de tempo.
#!/bin/bash
log() {
local level=$1
shift
local message="$*"
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo "[$timestamp] [$level] $message"
}
Se precisarmos de alta precisão, podemos usar date +"%Y-%m-%d %H:%M:%S.%N" para incluir nanossegundos, mas isso pode ser excessivo para a maioria dos casos. É importante garantir que o formato seja consistente para facilitar a análise posterior, por exemplo, com ferramentas como grep ou awk.
Boas práticas e observações finais
Ao implementar logging, considere as seguintes boas práticas: defina uma função de log centralizada, utilize níveis de severidade para controlar a verbosidade, inclua timestamps sempre, e direcione erros para stderr. Além disso, evite logs excessivos que possam sobrecarregar o sistema; use o nível DEBUG apenas em desenvolvimento. Lembre-se de que os logs podem conter informações sensíveis, então evite registrar dados confidenciais. Por fim, teste seu script com diferentes níveis e destinos para garantir que o comportamento seja o esperado.
Referências
- Bash Reference Manual
- GNU Coreutils: date
- Advanced Bash-Scripting Guide: Redirection
- Bash Manual: Redirections
- Bash Manual: Functions
- Bash Manual: Variables
Exercícios
- Crie uma função de log simples que imprima uma mensagem prefixada com o nível (INFO, WARN, ERROR) e a data/hora no formato YYYY-MM-DD HH:MM:SS.✓ Resposta:
#!/bin/bash log() { local level=$1 shift local message="$*" local timestamp=$(date +"%Y-%m-%d %H:%M:%S") echo "[$timestamp] [$level] $message" } log INFO "Mensagem informativa" log WARN "Aviso" log ERROR "Erro" - Implemente níveis de log com valores numéricos (DEBUG=0, INFO=1, WARN=2, ERROR=3) e uma variável que controla o nível mínimo exibido. Teste com diferentes níveis.✓ Resposta:
#!/bin/bash LEVEL_DEBUG=0 LEVEL_INFO=1 LEVEL_WARN=2 LEVEL_ERROR=3 CURRENT_LEVEL=$LEVEL_INFO log() { local level=$1 shift local message="$*" if [ $level -ge $CURRENT_LEVEL ]; then local level_name="" case $level in $LEVEL_DEBUG) level_name="DEBUG";; $LEVEL_INFO) level_name="INFO";; $LEVEL_WARN) level_name="WARN";; $LEVEL_ERROR) level_name="ERROR";; esac echo "[$level_name] $message" fi } log $LEVEL_DEBUG "Debug" log $LEVEL_INFO "Info" log $LEVEL_WARN "Warning" log $LEVEL_ERROR "Error" - Modifique a função de log para gravar as mensagens em um arquivo (ex.: script.log) e, para mensagens de erro, também enviar para stderr.✓ Resposta:
#!/bin/bash LOG_FILE="script.log" LEVEL_INFO=1 LEVEL_ERROR=3 log() { local level=$1 shift local message="$*" local timestamp=$(date +"%Y-%m-%d %H:%M:%S") local formatted="[$timestamp] [$level] $message" echo "$formatted" >> "$LOG_FILE" if [ $level -ge $LEVEL_ERROR ]; then echo "$formatted" >&2 else echo "$formatted" fi } log $LEVEL_INFO "Iniciando" log $LEVEL_ERROR "Falha crítica" - Crie uma função de log que aceite um nível como string (DEBUG, INFO, WARN, ERROR) e converta para o valor numérico correspondente, usando um array ou case.✓ Resposta:
#!/bin/bash # Mapeia níveis para números case $1 in DEBUG) level=0;; INFO) level=1;; WARN) level=2;; ERROR) level=3;; *) level=1;; # default INFO esac log() { local level_str=$1 shift local message="$*" local level_num case $level_str in DEBUG) level_num=0;; INFO) level_num=1;; WARN) level_num=2;; ERROR) level_num=3;; *) level_num=1;; esac local timestamp=$(date +"%Y-%m-%d %H:%M:%S") echo "[$timestamp] [$level_str] $message" } log INFO "Mensagem" log ERROR "Erro" - Escreva um script que use logging com timestamps e níveis, e que redirecione a saída padrão para um arquivo e a saída de erro para outro (usando redirecionamento no shell, não na função).✓ Resposta:
#!/bin/bash # Função de log simples log() { local level=$1 shift local message="$*" local timestamp=$(date +"%Y-%m-%d %H:%M:%S") echo "[$timestamp] [$level] $message" } log INFO "Iniciando script" log ERROR "Um erro ocorreu" log INFO "Finalizando"Para executar redirecionando, use no terminal:
./script.sh > out.log 2> err.log