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 sistema
  • LOG_ALERT (1) — ação imediata necessária
  • LOG_CRIT (2) — condições críticas
  • LOG_ERR (3) — erros
  • LOG_WARNING (4) — avisos
  • LOG_NOTICE (5) — eventos normais mas significativos
  • LOG_INFO (6) — informação geral
  • LOG_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 syslog podem 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 microssegundos
  • level — string do nível
  • module — 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 malloc em 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 .1 e 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, closelog e 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_PID e LOG_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.