Logging em scripts Bash

1. Por que fazer logging em scripts Bash?

Logging é uma prática essencial em scripts Bash por várias razões. Primeiramente, permite o rastreamento de execução — você pode ver exatamente o que aconteceu durante a execução do script, em que ordem e em que momento. Isso é crucial para depuração de erros, especialmente em scripts que rodam em produção ou em horários agendados (cron jobs).

Além disso, logs servem para auditoria de ações e conformidade. Em ambientes corporativos, é comum precisar registrar quem executou o quê e quando. Por fim, a diferenciação entre logs de sucesso, aviso e falha ajuda a priorizar problemas: um erro crítico merece atenção imediata, enquanto um aviso pode ser revisado depois.

2. Função de logging simples e reutilizável

Vamos criar uma função de logging básica que inclui timestamp, nome do script e nível da mensagem. Essa função pode ser colocada em um arquivo separado e reutilizada em vários scripts.

#!/bin/bash

# Função de logging simples
log() {
    local LEVEL="$1"
    local MESSAGE="$2"
    local TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
    local SCRIPT_NAME=$(basename "$0")
    echo "[$TIMESTAMP] [$SCRIPT_NAME] [$LEVEL] $MESSAGE"
}

# Uso da função
log "INFO" "Script iniciado"
log "WARN" "Variável X não definida, usando valor padrão"
log "ERROR" "Falha ao conectar ao banco de dados"

3. Níveis de severidade e formatação de mensagens

Definir níveis de severidade ajuda a categorizar a importância das mensagens. Os níveis comuns são:

  • DEBUG: Informações detalhadas para depuração
  • INFO: Informações normais de execução
  • WARN: Avisos sobre situações anormais que não impedem a execução
  • ERROR: Erros que afetam a funcionalidade, mas não interrompem o script
  • FATAL: Erros críticos que interrompem imediatamente o script

A formatação consistente segue o padrão: [DATA HORA] [NÍVEL] Mensagem. Opcionalmente, podemos adicionar cores no terminal para destacar visualmente cada nível.

#!/bin/bash

# Cores para terminal (opcional)
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

log() {
    local LEVEL="$1"
    local MESSAGE="$2"
    local TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
    local SCRIPT_NAME=$(basename "$0")

    case "$LEVEL" in
        DEBUG) COLOR="$BLUE" ;;
        INFO)  COLOR="$GREEN" ;;
        WARN)  COLOR="$YELLOW" ;;
        ERROR) COLOR="$RED" ;;
        FATAL) COLOR="$RED" ;;
        *)     COLOR="$NC" ;;
    esac

    echo -e "${COLOR}[$TIMESTAMP] [$SCRIPT_NAME] [$LEVEL] $MESSAGE${NC}"
}

log "INFO" "Processando arquivo..."
log "WARN" "Arquivo duplicado encontrado"
log "ERROR" "Falha na leitura do arquivo"

4. Redirecionamento de saída: stdout vs stderr

Em scripts Bash, é importante separar logs normais (stdout) de erros (stderr). Isso permite direcionar cada tipo de saída para destinos diferentes.

#!/bin/bash

log() {
    local LEVEL="$1"
    local MESSAGE="$2"
    local TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
    local SCRIPT_NAME=$(basename "$0")

    if [ "$LEVEL" = "ERROR" ] || [ "$LEVEL" = "FATAL" ]; then
        echo "[$TIMESTAMP] [$SCRIPT_NAME] [$LEVEL] $MESSAGE" >&2
    else
        echo "[$TIMESTAMP] [$SCRIPT_NAME] [$LEVEL] $MESSAGE"
    fi
}

log "INFO" "Operação normal"
log "ERROR" "Falha crítica"

# Redirecionamento para arquivos
# script.sh > app.log 2> error.log

O redirecionamento 2>&1 combina stderr com stdout, útil quando você quer tudo em um único arquivo.

5. Logging em arquivo com rotação básica

Para logging persistente, escrevemos em arquivo usando >> (append). Uma rotação básica pode ser implementada manualmente verificando o tamanho do arquivo.

#!/bin/bash

LOG_FILE="/var/log/meu_script.log"
MAX_LOG_SIZE=1048576  # 1 MB em bytes

log() {
    local LEVEL="$1"
    local MESSAGE="$2"
    local TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
    local SCRIPT_NAME=$(basename "$0")

    # Verifica tamanho do log e faz rotação se necessário
    if [ -f "$LOG_FILE" ]; then
        local FILE_SIZE=$(stat -c%s "$LOG_FILE" 2>/dev/null)
        if [ "$FILE_SIZE" -gt "$MAX_LOG_SIZE" ]; then
            mv "$LOG_FILE" "${LOG_FILE}.old"
            echo "[$TIMESTAMP] Log rotacionado" > "$LOG_FILE"
        fi
    fi

    echo "[$TIMESTAMP] [$SCRIPT_NAME] [$LEVEL] $MESSAGE" >> "$LOG_FILE"
    echo "[$TIMESTAMP] [$SCRIPT_NAME] [$LEVEL] $MESSAGE"
}

log "INFO" "Script em execução"

Para ambientes de produção, a ferramenta logrotate é mais robusta e recomendada, mas a implementação manual é útil para scripts simples.

6. Centralização de logs e boas práticas

Centralizar configurações de logging em variáveis globais facilita a manutenção. Boas práticas incluem:

  • Definir diretório e nome do arquivo de log como variáveis
  • Permitir configuração via argumentos de linha de comando
  • Evitar logging de informações sensíveis (senhas, tokens)
#!/bin/bash

# Configurações globais
LOG_DIR="/var/log/meu_app"
LOG_FILE="${LOG_DIR}/app.log"
LOG_LEVEL="INFO"  # Pode ser DEBUG, INFO, WARN, ERROR
VERBOSE=false

# Processa argumentos
while getopts "l:d:v" opt; do
    case $opt in
        l) LOG_LEVEL="$OPTARG" ;;
        d) LOG_DIR="$OPTARG"; LOG_FILE="${LOG_DIR}/app.log" ;;
        v) VERBOSE=true ;;
        *) echo "Uso: $0 [-l nível] [-d diretório] [-v]" >&2; exit 1 ;;
    esac
done

# Cria diretório se não existir
mkdir -p "$LOG_DIR"

log() {
    local LEVEL="$1"
    local MESSAGE="$2"
    local TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
    local SCRIPT_NAME=$(basename "$0")

    # Filtra por nível
    case "$LOG_LEVEL" in
        DEBUG) ;;
        INFO)  [ "$LEVEL" = "DEBUG" ] && return ;;
        WARN)  [ "$LEVEL" = "DEBUG" ] || [ "$LEVEL" = "INFO" ] && return ;;
        ERROR) [ "$LEVEL" != "ERROR" ] && [ "$LEVEL" != "FATAL" ] && return ;;
    esac

    echo "[$TIMESTAMP] [$SCRIPT_NAME] [$LEVEL] $MESSAGE" >> "$LOG_FILE"
    [ "$VERBOSE" = true ] && echo "[$TIMESTAMP] [$SCRIPT_NAME] [$LEVEL] $MESSAGE"
}

log "INFO" "Script configurado com nível $LOG_LEVEL"

7. Exemplo completo de script com logging

Abaixo, um script completo que integra todas as técnicas apresentadas:

#!/bin/bash

# ============================================
# Script de backup com logging estruturado
# ============================================

# Configurações
LOG_DIR="/var/log/backup"
LOG_FILE="${LOG_DIR}/backup_$(date +%Y%m%d).log"
BACKUP_DIR="/backup"
SOURCE_DIR="/dados"
MAX_LOG_SIZE=5242880  # 5 MB

# Cria diretórios necessários
mkdir -p "$LOG_DIR" "$BACKUP_DIR"

# Função de logging
log() {
    local LEVEL="$1"
    local MESSAGE="$2"
    local TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
    local SCRIPT_NAME=$(basename "$0")

    # Rotação de log
    if [ -f "$LOG_FILE" ]; then
        local FILE_SIZE=$(stat -c%s "$LOG_FILE" 2>/dev/null || echo 0)
        if [ "$FILE_SIZE" -gt "$MAX_LOG_SIZE" ]; then
            mv "$LOG_FILE" "${LOG_FILE}.old"
            echo "[$TIMESTAMP] Log rotacionado" > "$LOG_FILE"
        fi
    fi

    # Saída para arquivo
    echo "[$TIMESTAMP] [$SCRIPT_NAME] [$LEVEL] $MESSAGE" >> "$LOG_FILE"

    # Saída para terminal com cores
    case "$LEVEL" in
        INFO)  echo -e "\033[0;32m[$TIMESTAMP] [$LEVEL] $MESSAGE\033[0m" ;;
        WARN)  echo -e "\033[1;33m[$TIMESTAMP] [$LEVEL] $MESSAGE\033[0m" >&2 ;;
        ERROR) echo -e "\033[0;31m[$TIMESTAMP] [$LEVEL] $MESSAGE\033[0m" >&2 ;;
        FATAL) echo -e "\033[0;31m[$TIMESTAMP] [$LEVEL] $MESSAGE\033[0m" >&2 ;;
    esac
}

# Função para realizar backup
backup_directory() {
    local DIR="$1"
    local DEST="$2"
    local NAME=$(basename "$DIR")

    log "INFO" "Iniciando backup de $DIR para $DEST"

    if [ ! -d "$DIR" ]; then
        log "ERROR" "Diretório $DIR não existe"
        return 1
    fi

    tar -czf "${DEST}/${NAME}_$(date +%Y%m%d_%H%M%S).tar.gz" "$DIR" 2>> "$LOG_FILE"

    if [ $? -eq 0 ]; then
        log "INFO" "Backup de $DIR concluído com sucesso"
    else
        log "ERROR" "Falha no backup de $DIR"
        return 1
    fi
}

# Execução principal
log "INFO" "=== Início do script de backup ==="

backup_directory "$SOURCE_DIR" "$BACKUP_DIR"
BACKUP_STATUS=$?

if [ $BACKUP_STATUS -eq 0 ]; then
    log "INFO" "=== Backup concluído com sucesso ==="
else
    log "FATAL" "=== Backup falhou ==="
    exit 1
fi

log "INFO" "Script finalizado"

8. Considerações finais e próximos passos

O logging estruturado em scripts Bash traz benefícios significativos: facilita a depuração, fornece auditoria confiável e permite monitoramento automatizado. As técnicas apresentadas — funções reutilizáveis, níveis de severidade, redirecionamento adequado e rotação de logs — formam uma base sólida para qualquer script.

Para avançar, considere integrar seus logs com ferramentas externas como syslog (usando logger), journald (systemd) ou sistemas centralizados como ELK Stack. Além disso, explore artigos complementares desta série sobre debugging com set -x e tratamento de erros com trap para um controle ainda mais refinado.

Referências