Single‑header logger with colors, file:line, groups & timers.
Include the header everywhere; in exactly one.cfile#define CLOG_IMPLEMENTATIONbefore including.
- Repo: https://github.com/milchinskiy/c-log
- License: MIT
- Languages: C (C99+), C++ compatible
- Platforms: Linux, macOS, Windows
- Why C Log?
- Quick start
- Public API
- Log macros & levels
- Groups
- Timers
- Thread safety & locking
- Colors
- Runtime controls
- Compile‑time options
- Redirecting to a file descriptor
- Typical outputs
- Build notes & integration
- FAQ
- License
- Single header — drop
c-log.hinto your tree. - Zero heap allocations — fixed per‑thread buffer.
- Thread‑safe (opt‑in) — fast SRWLOCK/pthread mutex by default; spinlock optional.
- Colorful, readable prefixes with timestamp, level, file:line, thread id, optional group & build tag.
- Timers — nanosecond, microsecond, millisecond, or second units; convenient scope helper.
- No external deps beyond the standard C runtime and pthreads on POSIX (see notes).
// main.c
#define CLOG_IMPLEMENTATION
#include "c-log.h"
int main(void) {
clog_banner(); // "logger ready" or "build: ..."
log_info("Hello, %s!", "world");
log_warn_group("net", "retrying in %d ms", 200);
clog_set_level(CLOG_DEBUG); // show DEBUG and above
CLOG_SCOPE_TIME("demo work") {
// ... do some work ...
log_debug("working...");
}
log_error("oops: %d", 42);
return 0;
}Build (POSIX):
cc -std=c11 -O2 main.c -lpthread -o demoBuild (Windows, MSVC):
cl /O2 /std:c11 main.cOn Windows, the logger enables ANSI coloring for the console automatically when possible.
void clog_set_level(clog_level lvl);
clog_level clog_get_level(void);
int clog_get_fd(void);
void clog_set_fd(int fd);
void clog_banner(void);
// Timers (call‑site aware; prefer macros below):
void clogp_timer_start_(const char *file, int line, const char *label);
void clogp_timer_end_(const char *file, int line, const char *label);typedef enum { CLOG_TRACE = 0, CLOG_DEBUG, CLOG_INFO, CLOG_WARN, CLOG_ERROR, CLOG_FATAL } clog_level;Note: The
clogp_*timer functions are public for completeness, but you’ll usually call the convenience macros shown in Timers.
Each level has two forms: plain and grouped (*_group(group, ...)).
log_trace("..."); log_trace_group("subsys", "...");
log_debug("..."); log_debug_group("db", "...");
log_info("..."); log_info_group("clog", "...");
log_warn("..."); log_warn_group("net", "...");
log_error("..."); log_error_group("io", "...");
log_fatal("..."); log_fatal_group("core", "...");- Runtime threshold controls what is emitted (see
clog_set_level). - Compile‑time minimum controls what is compiled in (see
CLOG_COMPILETIME_MIN_LEVEL).
Below the compile‑time minimum, macros become((void)0)and carry zero cost.
Groups let you tag a log line with a sub‑system label that appears in the prefix:
2025-01-01 12:34:56.789 [INFO] [main.c:12] (tid:1234) [net] starting client
Use the *_group("net", "...") variants to set the group string.
Timers are call‑site aware and require no allocations. You can time a labeled section using either explicit start/end or the scope helper.
clog_start_time("load assets");
// ... work ...
clog_end_time("load assets"); // emits at DEBUG levelCLOG_SCOPE_TIME("parse file") {
// ... work ...
}Timer output unit is chosen automatically by duration:
< CLOG_TIMER_NS_MAX→ ns< CLOG_TIMER_US_MAX→ µs (or custom unit string)< CLOG_TIMER_MS_MAX→ ms- otherwise → s
Timer logs are emitted at DEBUG level. Ensure your runtime/compile‑time level allows
CLOG_DEBUG.
Capacity: Each thread has CLOG_TIMERS_MAX slots. If you exceed it, a warning is logged.
- Per‑thread scratch buffer (
CLOG_LINE_MAXbytes) and timer slots (CLOG_TIMERS_MAX) useCLOG_THREADLOCALstorage. - Emission is protected by a global lock when
CLOG_THREAD_SAFE=1.
Lock kinds (see also Locking choices):
CLOG_LOCK_KIND=1— Spinlock usingatomic_flagwith bounded spins (CLOG_SPIN_ITERS), yielding periodically.CLOG_LOCK_KIND=2— Mutex (default): SRWLOCK on Windows,pthread_mutex_ton POSIX.CLOG_LOCK_KIND=0— No locking (fastest, but not thread‑safe).
- Colors are enabled when
CLOG_COLOR=1and output is a TTY.- On Windows, the logger attempts to enable VT processing for the console handle.
- Set
CLOG_COLOR_FORCE=1to colorize even when not a TTY (useful for tools that capture ANSI). - Respect the standard
NO_COLORenvironment variable — if set and non‑empty, colors are disabled. - Level → color:
- TRACE gray, DEBUG cyan, INFO green, WARN yellow, ERROR red, FATAL magenta.
| Control | How | Notes |
|---|---|---|
| Change current level | clog_set_level(CLOG_DEBUG); |
Affects emission threshold. |
| Read current level | clog_get_level(); |
|
| Redirect output | clog_set_fd(fd); |
Pass a file descriptor (not FILE*). Color detection follows the current fd per call. |
| Banner | clog_banner(); |
Emits "logger ready" or "build: <CLOG_BUILD>" if provided. |
| Colors off via env | NO_COLOR=1 ./app |
Overrides any compile‑time default when CLOG_COLOR=1. |
You can define these macros at compile time (e.g., -DNAME=value) before including c-log.h.
| Macro | Default | Meaning |
|---|---|---|
CLOG_THREAD_SAFE |
1 |
Enable locking around writes (see lock kind). |
CLOG_LOCK_KIND |
2 |
0 none, 1 spin, 2 mutex (SRWLOCK / pthread). |
CLOG_SPIN_ITERS |
100 |
Spin iterations before yielding (kind=1). |
CLOG_LINE_MAX |
1024 |
Per‑thread output buffer size. Lines longer than this are truncated and tagged with "[TRUNC]". |
CLOG_TIMERS_MAX |
16 |
Timer slots per thread. |
CLOG_COLOR |
1 |
Enable color support (TTY‑aware). |
CLOG_COLOR_FORCE |
0 |
Force colors regardless of TTY. |
CLOG_WITH_LINE |
1 |
Include file:line in prefix. |
CLOG_WITH_TID |
1 |
Include thread id (tid:...). |
CLOG_TID_SHORT |
0 |
If 1, use low 24 bits as hex: (t#XXXXXX). |
CLOG_WITH_BUILD_IN_PREFIX |
0 |
If 1 and CLOG_BUILD is defined, include [build:<CLOG_BUILD>] in every prefix. |
CLOG_TIME_UTC |
0 |
If 1, timestamps are UTC; otherwise local time. |
| Macro | Default | Effect |
|---|---|---|
CLOG_DEFAULT_LEVEL |
CLOG_INFO |
Starting runtime threshold. Can be changed at run time via clog_set_level. You can override with -DCLOG_LEVEL=CLOG_DEBUG (shorthand). |
CLOG_COMPILETIME_MIN_LEVEL |
CLOG_TRACE |
Compile‑time elision threshold: log macros below this level compile to no‑ops. You can set via -DCLOG_MIN_LEVEL=CLOG_WARN. |
Precedence:
CLOG_DEFAULT_LEVELresolves toCLOG_LEVELif provided; otherwiseCLOG_INFO.
CLOG_COMPILETIME_MIN_LEVELresolves toCLOG_MIN_LEVELif provided; otherwiseCLOG_TRACE.
CLOG_LOCK_KIND |
Windows | POSIX | Notes |
|---|---|---|---|
0 |
none | none | Not thread‑safe; fastest. |
1 |
atomic_flag + SwitchToThread() |
atomic_flag + sched_yield() |
Bounded spin, CLOG_SPIN_ITERS spins before yield. |
2 (default) |
SRWLOCK |
pthread_mutex_t |
Recommended general choice. Link with -lpthread on POSIX. |
Prefix format is:
YYYY-MM-DD HH:MM:SS.mmm [LEVEL] (tid:123) <file:line> [group?] [build:?] message...
file:linecan be disabled withCLOG_WITH_LINE=0(then just[file]).- Thread id formatting can be short with
CLOG_TID_SHORT=1→(t#XXXXXX). - A build tag may be present if both
CLOG_WITH_BUILD_IN_PREFIX=1andCLOG_BUILD="..."are defined.
| Macro | Default | Effect |
|---|---|---|
CLOG_TIMER_NS_MAX |
1000ULL |
Durations < this emit in ns. |
CLOG_TIMER_US_MAX |
1000000ULL |
Durations < this emit in µs. |
CLOG_TIMER_MS_MAX |
1000000000ULL |
Durations < this emit in ms; otherwise s. |
CLOG_TIMER_UNIT_US |
"µs" |
Unit string for microseconds (override with -DCLOG_TIMER_UNIT_US="\"us\""). |
Timer labels are hashed (FNV‑1a 64‑bit) to identify slots. Collisions are possible but rare.
- Windows: uses
_write,GetLocalTime/GetSystemTime,QueryPerformanceCounter, and SRWLOCK; enables VT/ANSI for the console when possible. - POSIX: uses
write,clock_gettime(CLOCK_REALTIME | CLOCK_MONOTONIC),pthread_mutex_t(when locking), andisattyfor color detection. - C11/C++: thread‑local storage uses
CLOG_THREADLOCAL(_Thread_localor__declspec(thread)depending on platform).
#include <fcntl.h>
#include <unistd.h>
int fd = open("app.log", O_CREAT | O_TRUNC | O_WRONLY, 0644);
if (fd >= 0) {
clog_set_fd(fd);
log_info("this goes to app.log");
}- Pass a file descriptor (not
FILE*). If you needFILE*, grab its fd viafileno(fp). - On
CLOG_FATAL, the logger flushes the fd (fsyncon POSIX,_commiton Windows).
TTY (colorized):
2025-09-05 10:15:00.123 [INFO] (tid:4242) <main.c:10> logger ready
2025-09-05 10:15:00.125 [WARN] (tid:4243) <net.c:88> [net] reconnect in 200 ms
2025-09-05 10:15:00.128 [DEBUG] (tid:4242) <load.c:55> [timer] [472.331 µs]: parse file
Plain (no color):
2025-09-05 10:15:00.123 [INFO] (tid:4242) <main.c:10> logger ready
When a message exceeds CLOG_LINE_MAX, it is truncated and tagged: ...
Include c-log.h everywhere. In exactly one translation unit:
#define CLOG_IMPLEMENTATION
#include "c-log.h"# CMakeLists.txt
add_library(clog INTERFACE)
target_include_directories(clog INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
# Your app/library
add_executable(app main.c)
target_link_libraries(app PRIVATE clog)
target_compile_definitions(app PRIVATE CLOG_IMPLEMENTATION)
if (UNIX)
target_link_libraries(app PRIVATE pthread)
endif()- GCC/Clang: supports
__attribute__((format(printf,...)))for format checking. - MSVC: format checking attribute is ignored (harmless).
- On older glibc, you might need
-lrtforclock_gettime. Modern toolchains don’t. - Not async‑signal‑safe (uses
snprintf, locks, etc.). Avoid calling from signal handlers.
How do I disable colors in CI?
- Set the environment variable:
NO_COLOR=1. - Or compile with
-DCLOG_COLOR=0. - Or redirect output to a non‑TTY (colors auto‑disable unless
CLOG_COLOR_FORCE=1).
Why doesn’t my timer print?
- Timers emit at
CLOG_DEBUG. Ensure bothclog_set_level(CLOG_DEBUG)(runtime) andCLOG_COMPILETIME_MIN_LEVEL <= CLOG_DEBUG(compile‑time).
Can I log to stdout instead of stderr?
- Yes:
clog_set_fd(1);(stdout). Default is2(stderr).
What happens on partial writes or EINTR?
- The logger retries until all bytes are written (handles
EINTR).
Is it safe to use in multiple threads?
- Yes, when
CLOG_THREAD_SAFE=1(default). SetCLOG_LOCK_KINDper your needs.
Can I add my build id to every line?
- Define
-DCLOG_WITH_BUILD_IN_PREFIX=1 -DCLOG_BUILD="\"v1.2.3\"".
MIT — see LICENSE.
Levels
Runtime: clog_set_level(CLOG_TRACE|CLOG_DEBUG|CLOG_INFO|CLOG_WARN|CLOG_ERROR|CLOG_FATAL)
Compile (elide): -DCLOG_MIN_LEVEL=CLOG_WARN // strips calls below WARN at compile time
Default runtime: -DCLOG_LEVEL=CLOG_DEBUG // startup threshold
Colors
Enable: -DCLOG_COLOR=1 // default
Force TTY: -DCLOG_COLOR_FORCE=1 // enable even if not a TTY
Disable: -DCLOG_COLOR=0 or NO_COLOR=1 // env var wins
Prefix (order: timestamp [LEVEL] build? tid? <file:line> group?)
File:line: -DCLOG_WITH_LINE=1 // default (prints as <file:line>)
Thread id: -DCLOG_WITH_TID=1 // default
-DCLOG_TID_SHORT=1 // hex short form (t#XXXXXX)
UTC time: -DCLOG_TIME_UTC=1
Build tag: -DCLOG_WITH_BUILD_IN_PREFIX=1 -DCLOG_BUILD="\"hash\"" // emits [build:hash]
Locking
Thread-safe: -DCLOG_THREAD_SAFE=1 // default
Kind: -DCLOG_LOCK_KIND=2|1|0 // 2=mutex (default), 1=spin, 0=none
Spin loops: -DCLOG_SPIN_ITERS=100 // only for KIND=1
Buffers & timers
Line size: -DCLOG_LINE_MAX=1024
Timers: -DCLOG_TIMERS_MAX=16 // per-thread fixed slots
0 => timers become no-ops (API intact)
Units: -DCLOG_TIMER_UNIT_US="\"us\"" // default is "µs"
Ranges: -DCLOG_TIMER_NS_MAX=1000
-DCLOG_TIMER_US_MAX=1000000
-DCLOG_TIMER_MS_MAX=1000000000
Format checking (opt-in)
Enable GCC/Clang printf checks for literals:
-DCLOG_FORMAT_CHECK=1
(Default is 0 so variable format strings like `const char* f=...; log_info(f, ...)` won’t warn.)
Banner
clog_banner(): prints a header line without prefix/level (e.g., "=== build: <tag> ===" or "=== logger: ready ===")
— always shown, independent of current log level.