Skip to content

Architecture

Hugo edited this page Oct 15, 2026 · 2 revisions

Architecture

Source layout

Path Role
include/coretrace/logger.hpp Public API, including the log() templates
src/logger.cpp State, filtering, line formatting, output
src/logger_platform.hpp Platform interface: color support, stderr writes, process and thread IDs, UTC time, fork handlers
src/logger_posix.cpp POSIX backend (Linux, macOS)
src/logger_windows.cpp Windows backend
example/main.cpp Demo program, also run as a test
tests/ Test suite, see Testing
cmake/coretrace-loggerConfig.cmake.in Package configuration for find_package()

CMake compiles logger.cpp and one backend, chosen with WIN32, into the static library coretrace_logger, exported as coretrace::logger.

State

All state is process-wide, in src/logger.cpp:

  • Atomics: enabled flag, minimum level, sink pointer, thread-safe, timestamp and source-location flags.
  • A state mutex guards the prefix (64-byte buffer) and the module table (32 entries of 32 bytes). Neither allocates.
  • An output mutex serializes lines in thread-safe mode.
  • Both mutexes are reentrant: a std::mutex with a thread-local count of the guards held, locked when the count goes from 0 to 1. Not std::recursive_mutex, which a child of fork() cannot unlock: it checks the ID of its owner, and the child's thread has another one. They are never destroyed, so fork() still works from an atexit() handler or a static destructor that runs after the logger's.

Initialization

init_once() runs std::call_once on the first log() call, getter, filter setter, or fork(). It reads CT_LOG_LEVEL and CT_DEBUG, unless the level or the modules were already set through the API. That is how explicit calls take precedence over the environment.

The color decision (NO_COLOR, then whether stderr is a terminal) is made once, on first use. It is a lock-free std::atomic, not a function-local static, whose initialization guard a child of fork() could inherit as taken. When two threads decide at once, the first decision stored holds for both. The PID is not cached, so a child of fork() logs its own.

Path of a log line

  1. The log() template runs init_once(), then checks the enabled flag, the level and, for tagged messages, the module filter.
  2. It formats the message with std::vformat. A format error writes the fallback line through write_format_error(), and an empty message stops here.
  3. write_log_line() copies the prefix under the state mutex, then builds the complete line in a std::string: timestamp, PID, prefix, level, location, module, message, with color sequences when enabled.
  4. It takes the output mutex, when thread-safe, and passes the line to write_raw(). That reads the sink once and calls it, or falls back to platform::write_stderr().

Building the line before locking keeps the critical section short. Delivering it in one call guarantees that a line is never split or interleaved.

Fork

A child of fork() has only the thread that forked: a lock or an initialization another thread of the parent was holding stays held forever. The logger registers fork handlers through platform::at_fork():

  1. Before the fork, the forking thread runs init_once(), then takes the output mutex and the state mutex, in the order of a sink that logs.
  2. Fork handlers registered before the logger's run in between, in the same thread. They can call the logger, since the locks count as held by that thread.
  3. After the fork, the parent and the child release both mutexes.

Platform backends

Function POSIX Windows
stderr_supports_color() isatty(2) _isatty(), then enables virtual terminal processing on the console
write_stderr() write(2), retried on EINTR and on partial writes _write() in chunks
process_id() getpid() GetCurrentProcessId()
current_thread_id() gettid (Linux), pthread_threadid_np (macOS), otherwise pthread_self GetCurrentThreadId()
utc_timestamp() clock_gettime + gmtime_r system_clock + gmtime_s
at_fork() pthread_atfork() does nothing

Consequences for tests

State is global and initialization runs once, so a scenario that depends on startup (environment variables, colors, logging disabled by default) needs its own process. See Testing.

Clone this wiki locally