Module 02 — Logger

What you learn

How to write a thread-safe, level-filtered logger in C that outputs timestamped lines to both console and file — the pattern used throughout the DP codebase.

In a multi-lcore DPDK application, multiple CPU cores log concurrently. Without a thread-safe logger you get interleaved, unreadable output. The logger is initialized once at startup and then every subsystem — EAL, ports, Hyperscan, Kafka, workers — uses the same LOG_* macros.


Where this fits in the real application

main()
  │
  ├─► config_load()             (Module 01)
  │
  ├─► logger_init(level, file)  ← THIS MODULE
  │     reads level from config: "DEBUG" / "INFO" / "WARN" / "ERROR"
  │     opens log file from config: /var/log/dp_app/dp_app.log
  │
  ├─► LOG_INFO("EAL initializing...")
  ├─► rte_eal_init()            (Module 11)
  │
  ├─► LOG_INFO("Port 0 configuring...")
  ├─► port_init()               (Module 13)
  │
  ├─► LOG_INFO("Hyperscan compiling...")
  ├─► hs_init_global_scratch()  (Module 19-21)
  │
  └─► LOG_INFO("Startup complete, packet processing active")

Every module from 03 onwards includes logger.h and uses LOG_* macros.


Files

File Purpose
logger.h Struct, enum, macros, function declarations — included by every module
logger.c Implementation + demo startup sequence
Makefile Build rules (-lpthread required)

Build and run

make
./logger

Expected output (with ANSI colours in terminal):

[2024-06-02 14:32:01.123] [INFO ] [logger.c:180] === DP Application starting ===
[2024-06-02 14:32:01.124] [INFO ] [logger.c:181] Logger initialized: level=DEBUG
[2024-06-02 14:32:01.124] [DEBUG] [logger.c:184] Config: eal.cores=0-7, socket_mem=2048
...
[2024-06-02 14:32:01.130] [WARN ] [logger.c:210] Port 0: RX ring 80% full on queue 2
[2024-06-02 14:32:01.131] [ERROR] [logger.c:212] Hyperscan scratch alloc failed...
...
[2024-06-02 14:32:01.135] [WARN ] [logger.c:221] This WARN line IS printed

Key concepts in the code

1. Log format

[TIMESTAMP ms] [LEVEL] [file.c:line] message

Millisecond timestamps let operators correlate log events with packet captures (tcpdump/Wireshark) down to the millisecond.

2. Dual output

3. Level filtering

if (level < g_logger.level)
    return;    /* fast path — no lock, no I/O */

In production the level is INFO. Switching to DEBUG via log_set_level() at runtime (triggered by SIGUSR1) floods the log with per-packet detail — useful for diagnosing a live issue without restarting the application.

4. Thread safety

pthread_mutex_lock(&g_logger.lock);
/* write to stderr / file */
pthread_mutex_unlock(&g_logger.lock);

In DPDK, rte_log() uses a similar internal lock. The mutex is only held during the actual I/O — the vsnprintf() into message[] happens outside the lock so formatting time doesn’t block other lcores.

5. __attribute__((format(printf, 4, 5)))

Tells GCC to type-check the format string and variadic arguments of log_write() exactly like printf. Without this, a bug like LOG_INFO("count=%s", 42) compiles silently and corrupts output.

6. FATAL calls abort()

if (level == LOG_LEVEL_FATAL)
    abort();

abort() generates a SIGABRT which triggers a core dump (if ulimit -c unlimited is set). GDB can then load the core and show exactly which line triggered the fatal log.


Log level guide

Level When to use
LOG_DEBUG Per-packet detail, internal state. Off in production.
LOG_INFO Normal startup progress, config values, connections established.
LOG_WARN Recoverable issues: retry, fallback, near-limit conditions.
LOG_ERROR Failure in one subsystem but app continues (e.g., one lcore failed).
LOG_FATAL Unrecoverable: cannot allocate mempool, config missing. Calls abort().

Next module

Module 03 — Ring Buffer: A fixed-size, lock-free (SPSC) ring buffer — the manual equivalent of DPDK’s rte_ring. Understanding it first makes rte_ring immediately intuitive when you see it in Module 15.


Source files

File Download
logger.h logger.h
logger.c logger.c
Makefile Makefile