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

Exercícios

  1. 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"
    
  2. 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"
    
  3. 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"
    
  4. 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"
    
  5. 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