Logging estruturado: JSON logs do Bash para agregação
1. Por que logs estruturados em Bash?
O logging textual tradicional em Bash — mensagens soltas como [INFO] Script iniciado — é frágil para automação. Ferramentas como grep e awk conseguem extrair informações, mas falham quando o formato muda, caracteres especiais aparecem ou campos são adicionados. O parsing torna-se um pesadelo de manutenção.
Logs em JSON resolvem isso. Cada entrada é um objeto estruturado, legível por máquinas, com schema flexível e suporte nativo em agregadores como Elasticsearch, Loki e Splunk. Em pipelines CI/CD, scripts de automação e monitoramento, logs JSON permitem filtrar por nível, extrair métricas e correlacionar eventos sem reescrever parsers.
2. Construindo funções básicas de log JSON
A base é uma função que serializa campos em JSON de forma segura. Usar jq garante escaping correto de caracteres especiais:
#!/bin/bash
log_json() {
local level="$1"
local message="$2"
local timestamp
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
jq -n \
--arg ts "$timestamp" \
--arg lvl "$level" \
--arg msg "$message" \
'{timestamp: $ts, level: $lvl, message: $msg}'
}
# Uso
log_json "INFO" "Serviço iniciado com sucesso"
log_json "ERROR" "Falha na conexão: timeout de 30s"
A saída será:
{"timestamp":"2025-04-09T14:30:00Z","level":"INFO","message":"Serviço iniciado com sucesso"}
{"timestamp":"2025-04-09T14:30:01Z","level":"ERROR","message":"Falha na conexão: timeout de 30s"}
jq -n cria um novo objeto JSON, e --arg trata automaticamente aspas, barras invertidas e caracteres de controle na string.
3. Campos enriquecidos: contexto e metadados
Para rastrear a origem de cada log, adicione metadados do ambiente:
log_json() {
local level="$1"
local message="$2"
local timestamp
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
jq -n \
--arg ts "$timestamp" \
--arg lvl "$level" \
--arg msg "$message" \
--arg script "${0##*/}" \
--arg pid "$$" \
--arg host "$(hostname)" \
--arg user "$USER" \
'{
timestamp: $ts,
level: $lvl,
message: $msg,
script_name: $script,
pid: $pid,
hostname: $host,
user: $user
}'
}
Para rastreamento de execuções, gere um UUID por sessão:
EXECUTION_ID=$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid)
log_json() {
...
--arg exec_id "$EXECUTION_ID" \
'{
...
execution_id: $exec_id,
correlation_id: $exec_id
}'
}
Metadados customizados podem ser passados como argumentos nomeados:
log_json_with_meta() {
local level="$1"
local message="$2"
shift 2
local extra_args=()
while [[ $# -gt 0 ]]; do
extra_args+=(--arg "$1" "$2")
shift 2
done
...
jq -n "${extra_args[@]}" \
--arg ts "$timestamp" \
--arg lvl "$level" \
--arg msg "$message" \
'{
timestamp: $ts,
level: $lvl,
message: $msg,
service: $service,
endpoint: $endpoint
}'
}
4. Níveis de severidade e saídas múltiplas
Mapeie níveis para syslog e direcione stderr/stdout:
declare -A LOG_LEVELS=(
[DEBUG]=7
[INFO]=6
[WARN]=4
[ERROR]=3
[FATAL]=2
)
log_json() {
local level="$1"
local message="$2"
local log_num=${LOG_LEVELS[$level]:-6}
local min_level_num=${LOG_LEVELS[${LOG_LEVEL:-INFO}]:-6}
[[ $log_num -le $min_level_num ]] || return 0
local json_output
json_output=$(build_json "$level" "$message")
if [[ $log_num -le 4 ]]; then
echo "$json_output" >&2
else
echo "$json_output"
fi
# Saída simultânea para arquivo
echo "$json_output" >> /var/log/meu_script.log 2>/dev/null || true
}
Para rotação segura, use tee e logrotate:
log_json() {
...
echo "$json_output" | tee -a /var/log/meu_script.log >&2
}
5. Integração com agregadores de logs
Formate para Elasticsearch/OpenSearch com campos padrão:
build_json() {
local level="$1"
local message="$2"
jq -n \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ")" \
--arg lvl "$level" \
--arg msg "$message" \
'{
"@timestamp": $ts,
"@version": 1,
"level": $lvl,
"message": $msg,
"logger_name": "bash_script",
"thread_name": "main"
}'
}
Envio direto via HTTP para Logstash ou Loki:
send_to_loki() {
local json_payload="$1"
local loki_url="http://loki:3100/loki/api/v1/push"
curl -s -X POST "$loki_url" \
-H "Content-Type: application/json" \
-d "$json_payload" > /dev/null 2>&1 || {
log_json "WARN" "Falha ao enviar log para Loki"
}
}
Implemente buffer e retry com backoff exponencial:
buffer_and_send() {
local log_entry="$1"
local max_retries=3
local delay=1
for ((i=1; i<=max_retries; i++)); do
if send_to_loki "$log_entry"; then
return 0
fi
sleep $delay
delay=$((delay * 2))
done
echo "$log_entry" >> /tmp/log_buffer.txt
}
6. Performance e boas práticas
Evite subshells desnecessários. printf é mais rápido que echo para strings simples, mas jq é a escolha correta para JSON:
# Ruim (múltiplos subshells)
log_json() {
local ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
local msg=$(echo "$1" | jq -Rs .)
echo "{\"timestamp\":\"$ts\",\"message\":$msg}"
}
# Bom (cache de comandos externos)
HOSTNAME=$(hostname)
DATE_FORMAT="+%Y-%m-%dT%H:%M:%SZ"
log_json() {
local ts
printf -v ts '%(%Y-%m-%dT%H:%M:%SZ)T' -1
jq -n --arg ts "$ts" --arg msg "$1" '{timestamp: $ts, message: $msg}'
}
Cacheie valores imutáveis em variáveis globais:
readonly SCRIPT_NAME="${0##*/}"
readonly HOSTNAME=$(hostname)
readonly EXECUTION_ID=$(uuidgen)
Para rotação segura, configure logrotate:
/var/log/meu_script.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 640 root adm
postrotate
systemctl reload meu_script 2>/dev/null || true
endscript
}
7. Exemplo completo: script de monitoramento com logs JSON
#!/bin/bash
# Configurações
readonly SERVICES=("nginx" "postgresql" "redis")
readonly CHECK_INTERVAL=60
readonly LOG_FILE="/var/log/healthcheck.json"
readonly EXECUTION_ID=$(uuidgen)
readonly HOSTNAME=$(hostname)
log_json() {
local level="$1"
local message="$2"
local timestamp
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
jq -n \
--arg ts "$timestamp" \
--arg lvl "$level" \
--arg msg "$message" \
--arg exec "$EXECUTION_ID" \
--arg host "$HOSTNAME" \
'{
"@timestamp": $ts,
level: $lvl,
message: $msg,
execution_id: $exec,
hostname: $host,
logger: "healthcheck"
}' | tee -a "$LOG_FILE"
}
check_service() {
local service="$1"
if systemctl is-active --quiet "$service"; then
log_json "INFO" "Serviço $service está ativo"
return 0
else
log_json "ERROR" "Serviço $service está inativo"
return 1
fi
}
# Loop principal
log_json "INFO" "Monitoramento iniciado"
while true; do
for svc in "${SERVICES[@]}"; do
check_service "$svc"
done
sleep "$CHECK_INTERVAL"
done
Para extrair métricas dos logs:
# Contagem de erros por serviço
cat /var/log/healthcheck.json | jq -r '
select(.level == "ERROR") |
.message |
capture("Serviço (?<service>[a-z]+) está inativo") |
.service
' | sort | uniq -c | sort -rn
Integração com alertas via pipe:
tail -f /var/log/healthcheck.json | while read line; do
level=$(echo "$line" | jq -r '.level')
if [[ "$level" == "ERROR" ]]; then
message=$(echo "$line" | jq -r '.message')
curl -s -X POST "https://hooks.slack.com/services/TOKEN" \
-H "Content-Type: application/json" \
-d "{\"text\":\"$message\"}"
fi
done
8. Armadilhas comuns e depuração
Problemas de encoding: logs com caracteres UTF-8 podem quebrar parsers. Use jq -Rs para garantir escaping:
message=$(printf '%s' "$raw_message" | jq -Rs .)
Logs muito grandes: comprima inline com gzip e faça chunking:
log_json() {
...
echo "$json_output" | gzip >> /var/log/compressed.log.gz
}
Testes unitários com bats:
#!/usr/bin/env bats
setup() {
source ./log_functions.sh
}
@test "log_json gera JSON válido" {
run log_json "INFO" "teste"
[ "$status" -eq 0 ]
echo "$output" | jq empty
}
@test "log_json respeita LOG_LEVEL" {
LOG_LEVEL=ERROR run log_json "INFO" "deve ser ignorado"
[ -z "$output" ]
}
Logs JSON em Bash não são apenas uma conveniência — são uma necessidade para sistemas modernos. Com funções bem construídas, integração nativa com agregadores e boas práticas de performance, seus scripts se tornam cidadãos de primeira classe em pipelines de observabilidade.
Referências
- jq Manual (stedolan.github.io) — Documentação oficial do jq, a ferramenta essencial para manipulação de JSON em shell scripts
- Bash Reference Manual (gnu.org) — Documentação oficial do Bash, incluindo expansão de parâmetros e redirecionamento
- Logstash Configuration (elastic.co) — Guia oficial de configuração do Logstash para ingestão de logs JSON
- Loki API Documentation (grafana.com) — Documentação da API HTTP do Loki para envio de logs estruturados
- bats-core: Bash Automated Testing System (github.com) — Framework de testes unitários para Bash, útil para validar funções de logging
- logrotate man page (linux.die.net) — Manual completo do logrotate para rotação segura de arquivos de log
- OpenSearch JSON Output (opensearch.org) — Documentação do OpenSearch sobre ingestão de logs JSON e análise