Logging estruturado com syslog ou custom logger
1. Introdução ao Logging em C
Em sistemas embarcados e servidores de alto desempenho, o logging não é um luxo — é uma necessidade operacional. Quando um processo crasha à 3h da manhã ou um buffer transborda em uma linha de produção, o log salva horas de debugging. A abordagem ingênua com printf espalhado pelo código gera ruído, perde contexto e frequentemente bloqueia a execução. Logging estruturado, por outro lado, organiza cada evento em campos previsíveis (timestamp, nível, módulo, mensagem), permitindo filtragem e análise automatizada.
Em C, duas estratégias principais se destacam: o syslog padrão POSIX, disponível em praticamente todos os Unixes, e a implementação de um logger customizado, que oferece controle total sobre formato e destino. Este artigo explora ambas, com ênfase em como construir um logger customizado que produza saída JSON — formato ideal para ingestão por sistemas modernos como ELK Stack ou Graylog.
2. Fundamentos do syslog
O syslog é o mecanismo de logging do sistema operacional, padronizado pelo POSIX. Sua API é enxuta:
#include <syslog.h>
void openlog(const char *ident, int option, int facility);
void syslog(int priority, const char *format, ...);
void closelog(void);
openlog define um identificador (nome do programa), opções como LOG_PID (incluir PID) e a facility (categoria do processo: LOG_USER, LOG_DAEMON, LOG_LOCAL0 a LOG_LOCAL7). A função syslog recebe uma prioridade que combina facility e nível de severidade:
LOG_EMERG(0) — pânico do sistemaLOG_ALERT(1) — ação imediata necessáriaLOG_CRIT(2) — condições críticasLOG_ERR(3) — errosLOG_WARNING(4) — avisosLOG_NOTICE(5) — eventos normais mas significativosLOG_INFO(6) — informação geralLOG_DEBUG(7) — depuração
Exemplo prático de uso:
#include <syslog.h>
#include <stdio.h>
int main(void) {
openlog("meuapp", LOG_PID | LOG_CONS, LOG_USER);
syslog(LOG_INFO, "Servidor iniciado na porta %d", 8080);
syslog(LOG_ERR, "Falha ao abrir arquivo %s: %m", "/etc/config");
closelog();
return 0;
}
O especificador %m é expandido para a mensagem de erro atual (strerror(errno)). Por padrão, as mensagens vão para /var/log/syslog ou são gerenciadas pelo daemon rsyslogd/journald.
3. Limitações do syslog e motivação para um logger customizado
Apesar de sua onipresença, o syslog apresenta limitações severas para aplicações modernas:
- Formato fixo: a mensagem é uma string plana. Não há campos nomeados, timestamp padronizado ou estrutura hierárquica. Análise automatizada exige parsing de regex frágil.
- Destino único: a mensagem sempre vai para o socket
/dev/log. Redirecionar para arquivo próprio ou stdout exige configuração externa. - Sem contexto rico: não é fácil incluir metadados como ID de requisição, nome de usuário ou parâmetros de função.
- Desempenho variável: chamadas
syslogpodem bloquear se o daemon estiver ocupado.
Essas limitações motivam a criação de um logger customizado que produza saída estruturada (JSON), permita múltiplos destinos e ofereça controle fino sobre formatação e desempenho.
4. Projetando um logger customizado em C
Um logger customizado deve ser modular e thread-safe. Começamos definindo níveis de log e uma estrutura de handler:
typedef enum {
LOGLVL_TRACE = 0,
LOGLVL_DEBUG,
LOGLVL_INFO,
LOGLVL_WARN,
LOGLVL_ERROR,
LOGLVL_FATAL
} log_level_t;
typedef struct {
FILE *file; /* arquivo de log (pode ser NULL) */
int use_syslog; /* fallback para syslog */
log_level_t min_level;
pthread_mutex_t mutex;
} logger_t;
Inicialização e destruição com proteção de mutex:
#include <pthread.h>
#include <stdarg.h>
static logger_t g_logger = {NULL, 0, LOGLVL_INFO, PTHREAD_MUTEX_INITIALIZER};
void logger_init(const char *filepath, log_level_t min_level, int use_syslog) {
pthread_mutex_lock(&g_logger.mutex);
if (filepath) {
g_logger.file = fopen(filepath, "a");
if (!g_logger.file) {
perror("fopen log file");
}
}
g_logger.min_level = min_level;
g_logger.use_syslog = use_syslog;
if (use_syslog) openlog("customapp", LOG_PID, LOG_USER);
pthread_mutex_unlock(&g_logger.mutex);
}
void logger_destroy(void) {
pthread_mutex_lock(&g_logger.mutex);
if (g_logger.file) fclose(g_logger.file);
if (g_logger.use_syslog) closelog();
g_logger.file = NULL;
pthread_mutex_unlock(&g_logger.mutex);
}
5. Logging estruturado: formato JSON e campos-chave
Para logging estruturado, adotamos JSON como formato de saída. Cada entrada contém:
timestamp— ISO 8601 com microssegundoslevel— string do nívelmodule— módulo do código (ex: "network", "storage")message— descrição do evento- Campos extras opcionais (pares chave-valor)
Geração de timestamp com clock_gettime:
#include <time.h>
void timestamp_iso8601(char *buf, size_t size) {
struct timespec ts;
struct tm tm;
clock_gettime(CLOCK_REALTIME, &ts);
localtime_r(&ts.tv_sec, &tm);
strftime(buf, size, "%Y-%m-%dT%H:%M:%S", &tm);
int off = strlen(buf);
snprintf(buf + off, size - off, ".%06ld", ts.tv_nsec / 1000);
}
Função auxiliar para escrever pares chave-valor em JSON:
void json_kv(FILE *fp, const char *key, const char *value, int last) {
fprintf(fp, "\"%s\":\"%s\"%s", key, value, last ? "" : ",");
}
6. Exemplo completo: logger customizado com saída JSON
A função central de logging recebe nível, módulo, mensagem e pares chave-valor via argumentos variádicos:
#include <stdarg.h>
#include <string.h>
void logger_log(log_level_t level, const char *module, const char *fmt, ...) {
if (level < g_logger.min_level) return;
pthread_mutex_lock(&g_logger.mutex);
char timestamp[64];
timestamp_iso8601(timestamp, sizeof(timestamp));
char msg[1024];
va_list args;
va_start(args, fmt);
vsnprintf(msg, sizeof(msg), fmt, args);
va_end(args);
const char *level_str[] = {"TRACE","DEBUG","INFO","WARN","ERROR","FATAL"};
/* Saída JSON */
FILE *out = g_logger.file ? g_logger.file : stdout;
fprintf(out, "{");
json_kv(out, "timestamp", timestamp, 0);
json_kv(out, "level", level_str[level], 0);
json_kv(out, "module", module, 0);
json_kv(out, "message", msg, 1);
fprintf(out, "}\n");
fflush(out);
/* Fallback para syslog */
if (g_logger.use_syslog) {
int sys_prio[] = {LOG_DEBUG, LOG_DEBUG, LOG_INFO, LOG_WARNING, LOG_ERR, LOG_CRIT};
syslog(sys_prio[level], "[%s] %s", module, msg);
}
pthread_mutex_unlock(&g_logger.mutex);
}
Macros para facilitar o uso:
#define LOG_TRACE(mod, fmt, ...) logger_log(LOGLVL_TRACE, mod, fmt, ##__VA_ARGS__)
#define LOG_INFO(mod, fmt, ...) logger_log(LOGLVL_INFO, mod, fmt, ##__VA_ARGS__)
#define LOG_ERROR(mod, fmt, ...) logger_log(LOGLVL_ERROR, mod, fmt, ##__VA_ARGS__)
Exemplo de uso:
int main(void) {
logger_init("/var/log/minhaapp.json", LOGLVL_DEBUG, 1);
LOG_INFO("main", "Servidor iniciado na porta %d", 8080);
LOG_ERROR("network", "Timeout na conexão com %s", "192.168.1.1");
logger_destroy();
return 0;
}
Saída gerada:
{"timestamp":"2025-04-08T14:23:01.123456","level":"INFO","module":"main","message":"Servidor iniciado na porta 8080"}
{"timestamp":"2025-04-08T14:23:05.987654","level":"ERROR","module":"network","message":"Timeout na conexão com 192.168.1.1"}
7. Boas práticas e considerações de desempenho
- Buffers estáticos: evite
mallocem caminhos críticos. Use buffers na stack ou reutilizáveis por thread (thread-local storage). No exemplo acima,msg[1024]é suficiente para 99% dos casos. - Log assíncrono: para aplicações de baixa latência, implemente uma fila circular (
ring buffer) onde threads produtoras depositam mensagens e uma thread consumidora as escreve em lote. Use variáveis de condição para sinalização. - Configuração externa: permita que nível mínimo e caminho do arquivo sejam definidos por variáveis de ambiente (ex:
LOG_LEVEL=DEBUG,LOG_FILE=/tmp/app.log) ou arquivo INI. Isso evita recompilação para mudanças em produção. - Rotação de arquivos: implemente rotação simples baseada em tamanho. Antes de escrever, verifique se
ftell(file) > MAX_SIZE; se sim, feche, renomeie com sufixo.1e abra novo arquivo.
8. Conclusão e próximos passos
A escolha entre syslog e logger customizado depende do contexto. Syslog é ideal para scripts, ferramentas de sistema e ambientes onde a infraestrutura de log já está consolidada (ex: journald corporativo). O logger customizado com saída JSON brilha em aplicações que exigem análise automatizada, múltiplos destinos e controle fino de formato.
Para integrar com sistemas de monitoramento, considere enviar os logs JSON via UDP para um coletor central (ex: Logstash ou Graylog). Uma thread separada pode ler a fila circular e enviar datagramas, mantendo a latência da aplicação baixa.
Como próximos passos, explore artigos vizinhos desta série: parsing de JSON em C para ler configurações, construção de CLI para controle de log em tempo real, e plugins para estender o logger com novos destinos (ex: syslog como fallback, Redis, Kafka).
Referências
- OpenGroup - syslog.h — Documentação oficial POSIX da API syslog, incluindo
openlog,syslog,closeloge todos os níveis/facilities. - GNU C Library - Syslog — Manual da glibc sobre syslog, com exemplos detalhados e explicação de opções como
LOG_PIDeLOG_CONS. - RFC 5424 - The Syslog Protocol — Especificação IETF do protocolo syslog, útil para entender formato estruturado e transporte.
- Logging in C with syslog — Tutorial da IBM sobre uso de syslog em C, incluindo tratamento de erros e configuração de facility.
- Building a Custom Logger in C — Discussão no Code Review Stack Exchange sobre implementação de logger customizado, com críticas de desempenho e segurança.
- JSON Logging Best Practices — Artigo da Honeycomb sobre boas práticas de logging estruturado em JSON, aplicável a qualquer linguagem incluindo C.
- Thread-Safe Logging with Mutexes in C — Tutorial da GeeksforGeeks sobre sincronização com mutex, essencial para implementar logger concorrente.