A high-performance C logging library with buffered I/O, log rotation, log levels, and remote logging support.
- Buffered writes — logs are accumulated in a 64 KB in-memory buffer and flushed every 10 ms or when the buffer reaches 32 KB, drastically reducing I/O syscalls compared to line-by-line file writes
- Log rotation — rotate log files by size, minute, hour, or day; old files are renamed with index suffixes automatically
- Log expiration — automatically delete log files older than a configurable retention period
- Nine log levels —
FATAL,ERROR,WARN,INFO,NOTICE,DEBUG,TRACE,USER1,USER2; levels can be toggled at runtime - Remote UDP logging — forward log messages to a remote
dlog_serverover UDP - Thread-safe — every log write is protected by a per-instance
pthread_mutex - Fork offload — optionally fork a child process to perform file rotation and deletion, keeping the main process latency low
- Stack backtrace —
dlog_backtrace()captures and logs a full call stack usingbacktrace_symbols - Fatal callback — register a
dlog_on_fatalcallback to be invoked on everylog_fatalcall (e.g. to send an alert) - Automatic cleanup — registers an
atexithandler that flushes and closes all open log instances on normal exit - Standalone log server —
dlog_serveris a ready-to-use UDP daemon that receives and persists remote log messages
| Macro | Flag | Description |
|---|---|---|
log_vip |
always on | Critical messages, bypasses the level flag |
log_fatal |
DLOG_FATAL |
Fatal errors; also triggers dlog_on_fatal callback |
log_error |
DLOG_ERROR |
Non-fatal errors |
log_warn |
DLOG_WARN |
Warnings |
log_info |
DLOG_INFO |
Informational messages |
log_notice |
DLOG_NOTICE |
Notable events |
log_debug |
DLOG_DEBUG |
Debug output |
log_trace |
DLOG_TRACE |
Fine-grained tracing |
log_user1 |
DLOG_USER1 |
User-defined level 1 |
log_user2 |
DLOG_USER2 |
User-defined level 2 |
log_exception |
DLOG_FATAL |
Fatal + automatic stack backtrace |
Each macro prepends the source file, line number, and function name automatically:
[2024-01-15 12:34:56.789012] [error]main.c:42(process_request): connection refused
# Build the test binary and the log server
make
# Build only the test binary
gcc -o dlog_test -g -Wall dlog.c test.c -D DEBUG -rdynamic
# Build only the log server
gcc -o dlog_server dlog_server.c dlog.c -D DLOG_SERVER -g -Wall -O2#include "dlog.h"
int main(void)
{
// Create a log handler: rotate daily, no size limit, keep 30 days
dlog_t *log = dlog_init("logs/app", DLOG_SHIFT_BY_DAY, 0, 0, 30);
if (log == NULL) {
fprintf(stderr, "dlog_init failed\n");
return 1;
}
// Set as the default log and enable desired levels
default_dlog = log;
default_dlog_flag = DLOG_FATAL | DLOG_ERROR | DLOG_WARN | DLOG_INFO;
log_info("server started");
log_error("something went wrong: %s", strerror(errno));
log_fatal("unrecoverable error, shutting down");
// Flush remaining buffer on exit (also done automatically via atexit)
dlog_flush_all();
return 0;
}dlog_t *dlog_init(const char *base_name, int flag, size_t max_size,
int log_num, int keep_time);| Parameter | Description |
|---|---|
base_name |
Path prefix for log files, e.g. "logs/app" |
flag |
Shift type combined with optional flags (see below) |
max_size |
Maximum file size in bytes before rotation (0 = unlimited) |
log_num |
Maximum number of rotated files to keep (0 = unlimited) |
keep_time |
Number of time units (days/hours/minutes) to retain old files |
Shift types (mutually exclusive, required):
| Constant | Description |
|---|---|
DLOG_SHIFT_BY_SIZE |
Rotate when file reaches max_size |
DLOG_SHIFT_BY_MIN |
Rotate every minute |
DLOG_SHIFT_BY_HOUR |
Rotate every hour |
DLOG_SHIFT_BY_DAY |
Rotate every day |
Optional flags (OR-combined with shift type):
| Flag | Description |
|---|---|
DLOG_USE_FORK |
Fork a child process for rotation and deletion |
DLOG_NO_CACHE |
Write to disk immediately on every log call |
DLOG_REMOTE_LOG |
Send logs to a remote UDP server (pass struct sockaddr_in * as base_name) |
DLOG_NO_TIMESTAMP |
Omit the timestamp prefix from each line |
// Examples
dlog_init("app", DLOG_SHIFT_BY_DAY, 0, 0, 30);
dlog_init("app", DLOG_SHIFT_BY_SIZE | DLOG_USE_FORK, 500*1024*1024, 10, 0);
dlog_init("app", DLOG_SHIFT_BY_HOUR | DLOG_NO_CACHE, 0, 24, 7);// Write a formatted message (thread-safe)
int dlog(dlog_t *log, const char *fmt, ...);
int dlogv(dlog_t *log, const char *fmt, va_list ap);
// Write to stderr (unbuffered, no log object needed)
void dlog_stderr(const char *fmt, ...);
// Write to syslog
void dlog_syslog(const char *fmt, ...);
// Log a stack backtrace
void dlog_backtrace(dlog_t *log);// Parse a comma/space-separated level string into a bitmask
// Accepts: "fatal", "error", "warn", "info", "notice", "debug", "trace", "user1", "user2"
int dlog_read_flag(char *str);
// Example
default_dlog_flag = dlog_read_flag("fatal, error, warn, info");
// Dynamically raise or lower the active log level by one step
void dlog_level_up(void);
void dlog_level_down(void);// Flush a single log to disk now
void dlog_flush(dlog_t *log);
// Flush all open logs to disk (safe to call from a signal handler)
void dlog_flush_all(void);
// Check whether a log needs flushing (call from main loop or timer)
void dlog_check(dlog_t *log, struct timeval *tv);
void dlog_check_all(void);// Close and free a log instance
int dlog_fini(dlog_t *log);
// Number of currently open log instances
int dlog_opened_num(void);To prevent log loss on crashes, catch SIGSEGV with SA_RESETHAND and call dlog_flush_all:
#include <signal.h>
void sigsegv_handler(int signo)
{
dlog_flush_all();
}
struct sigaction sa = { .sa_handler = sigsegv_handler, .sa_flags = SA_RESETHAND };
sigaction(SIGSEGV, &sa, NULL);// Called on every log_fatal invocation, e.g. to send an alert or notify a monitor
extern dlog_on_fatal_cb dlog_on_fatal;
int my_alert(const char *fmt, ...)
{
// send alert...
return 0;
}
dlog_on_fatal = my_alert;dlog supports forwarding log messages to a remote host over UDP.
// Pass "host:port" as base_name; dlog detects the colon and parses it
dlog_t *log = dlog_init("192.168.1.100:9000", DLOG_SHIFT_BY_DAY, 0, 0, 0);struct sockaddr_in addr = { 0 };
addr.sin_family = AF_INET;
inet_aton("192.168.1.100", &addr.sin_addr);
addr.sin_port = htons(9000);
dlog_t *log = dlog_init((const char *)&addr, DLOG_REMOTE_LOG | DLOG_SHIFT_BY_DAY, 0, 0, 0);int fd = socket(AF_INET, SOCK_DGRAM, 0);
// configure fd ...
dlog_set_sockfd(log, fd);Usage:
-h --help
-b --base base_name Log file path prefix (required)
-t --type shift_type size / day / hour / min (default: day)
-s --size max_size Max file size in bytes
-n --num log_num Max number of rotated files
-k --keep keep_time Retention period
-p --port port UDP port to listen on (required)
-a --addr Prepend sender IP:port to each message
-d --daemon Run as a background daemon
# Listen on port 9000, rotate daily, keep 30 days
./dlog_server -b logs/remote -t day -p 9000 -k 30 -dLog files are named <base_name><suffix> where the suffix encodes both the time period and the rotation index:
| Shift type | Example filenames |
|---|---|
| By size | app.log, app.log.1, app.log.2 |
| By day | app.20240115.log, app.20240114.log.1 |
| By hour | app.2024011512.log |
| By minute | app.202401151234.log |
- The 64 KB write buffer and 10 ms flush interval keep log overhead minimal for high-throughput services.
- Use
DLOG_USE_FORKto offload file rotation and deletion to a child process, eliminating blocking I/O in the main thread. - Avoid
DLOG_NO_CACHEin latency-sensitive paths; usedlog_check_all()in your event loop instead.