Quill is a C++17 asynchronous low-latency logging library.
Main design goals:
- keep frontend logging overhead as low as possible,
- move formatting and I/O to a backend worker thread,
- preserve timestamp ordering across threads,
- provide a practical API without compromising hot-path performance.
The project is performance-sensitive. Many design choices prioritize low frontend latency over extra convenience or extra validation at every call site.
Important directories:
include/quill/: public headers and most implementation.docs/: Sphinx docs and API guidance.test/unit_tests/: focused component tests.test/integration_tests/: end-to-end behavioral tests.examples/: usage examples.benchmarks/: compile-time and runtime benchmarks.
The library has two main parts:
- Frontend
- User threads log via macros or macro-free APIs.
- Each thread writes into its own thread-local SPSC queue.
- The hot path serializes arguments and stores metadata references.
- Backend
- A single backend worker thread polls frontend queues.
- It orders events by timestamp.
- It performs formatting and sink I/O.
When reviewing or changing code, always ask:
- Is this on the frontend hot path or backend path?
- Does this add cost to every log statement?
- Does this move work from backend to frontend?
- Does this change a documented user contract?
- Before fixing a "bug", trace all call sites — the invariant may be maintained by callers rather than the function itself.
- Single backend worker thread.
Loggerobjects are thread-safe for logging.- Logger configuration is effectively immutable after creation; recreate a logger instead of mutating it in place.
Frontend::remove_logger_blocking()must only be used while the backend is running.- Once a logger is removed, that
Logger*must not be used again. - Removing the same logger from multiple threads is unsupported.
- The project supports both exception and no-exception builds.
- Ideally used as a static library, but some users integrate it through shared-library /
.soboundaries. Keep shared-library pitfalls in mind as well, especially singleton lifetime, duplicated state across DSOs, symbol visibility, and shutdown behavior. - If you change public behavior, keep headers, docs, tests, and changelog aligned.
Respect the library layering:
- Avoid introducing dependencies from lower-level code to higher-level code.
- Flag dependency inversions in reviews.
quill/coreis part of the frontend-facing surface and should remain self-contained: it may depend onquill/coreandquill/bundled, but not on other layers.- For normal user-side logging, the intended lightweight include surface is
quill/Logger.handquill/LogMacros.h. Be very careful with every include reachable from those headers, including standard library headers. Any extra#includethere propagates into user translation units and increases compile-time cost and header pollution.
The bundled fmt copy under include/quill/bundled/fmt/ is vendored source with Quill-local patches. Treat each upgrade
as two separate jobs: first identify the upstream delta, then port the Quill-specific changes.
- Identify the current bundled fmt version from
FMTQUILL_VERSIONininclude/quill/bundled/fmt/base.h; the value uses fmt'smajor * 10000 + minor * 100 + patchencoding. Pick the target version from the official fmt releases at https://github.com/fmtlib/fmt/releases. - Download and unpack the official current and target fmt release archives outside
include/quill/bundled/fmt/. Compare the official current release with Quill's bundled tree to find local patches, then compare the target release with the official current release to understand upstream changes. - Replace the vendored fmt files from the official target release, then run:
python3 scripts/rename_libfmt.py include/quill/bundled/fmt FMTQUILL fmtquill - Preserve upstream formatting in bundled fmt files. Do not run clang-format or broad formatting tools over vendored fmt; port Quill patches as the smallest necessary edits.
- Reapply and re-check Quill-local changes that still apply to the target fmt version:
- In
base.h, force header-only mode withFMTQUILL_HEADER_ONLY. - In
base.h, keep the end-of-fileformat.hinclude removed or disabled when it is only included becauseFMTQUILL_HEADER_ONLYis set. - Keep the buffer
appendfast path that usesmemcpy(ptr_ + size_, begin, count * sizeof(T))whenstd::is_same<T, U>::value, with the element-wise copy fallback when the types differ. - In
chrono.h, keep thestd::microsuffix as"us"unless upstream already matches that behavior. - In
format.h, retain only the warning suppressions still needed by supported compilers, such as GCC/Clang-Wfloat-equaland GCC-Wstringop-overflowfalse positives. - Keep
is_fast_float<T>::valuein places where supported compilers require the type-trait form; if upstream changes this code, confirm with a targeted compile instead of applying a blind search-and-replace. - Search the previous local diff for any other Quill-specific compiler workarounds, diagnostics, ABI/header-only changes, namespace changes, or performance fixes and port the ones that remain applicable.
- In
- After porting, inspect the diff to confirm it contains only the upstream fmt update plus intentional Quill-local patches, then run the narrowest relevant build or test target for the touched code.
- The Sphinx docs live under
docs/. - Any standalone runnable sample in a
.rstfile (anything withmain(), a class definition, or that the reader could copy-paste and compile as-is) MUST live in its own compilable.cppfile underdocs/snippets/, added todocs/snippets/CMakeLists.txt, and referenced from the.rstvia.. literalinclude:: snippets/<file>.cpp. Never write full examples as inline.. code-block:: cppblocks — CI must be able to compile them so they cannot silently drift from the real API. - When adding a new docs page with a runnable sample, create the snippet file first, confirm it builds under
QUILL_BUILD_EXAMPLES=ON, and only then wire it into the.rstwith aliteralincludedirective. - Short illustrative fragments (a macro call, a function signature, a few lines showing an API shape) can stay as inline
.. code-block:: cppsince they are not standalone programs.
- Each new feature or fix should have a corresponding test, either a unit test or a regression/integration test.
- Sometimes consider extending an existing test with one more assertion or log statement instead of adding a new one.
- Unit test files may contain multiple
TEST_CASEs when the scenarios are closely related. - Each integration test file should contain exactly one
TEST_CASE. - Integration/regression test files should be self-contained. Do not add shared helper headers for them; keep any
test-only setup local to the
.cppfile even if that means a small amount of duplication. - Integration tests must use unique logger names, test names, and log/output filenames so they can run safely in parallel without colliding through shared files or global state.
- For conditionally-skipped tests (platform-specific,
QUILL_NO_EXCEPTIONS, etc.), keep theTEST_CASEitself unguarded and put the#ifinside the test body. On unsupported configurations, return early from the test instead of wrapping the whole test declaration in preprocessor guards. - If the behavior is user-visible or easy to regress, prefer an end-to-end test in addition to a narrow unit test.
- New features and tests should compile and run across supported platforms and toolchains, including Linux, macOS, and Windows, and major compilers such as GCC, Clang, and Intel LLVM.
- Never attempt a full build of all targets unless the user explicitly asks for one. Full builds are slow on this project, so only build the specific target(s) touched by the change (e.g. a single snippet, unit test, or integration test) and let the user drive any broader build.
- The repo is C++17 only.
- Follow existing codebase patterns.
- Prefer readable, maintainable, low-overhead code.
- Use descriptive names.
- Prefer const-correctness,
constexpr, and RAII where appropriate. - Use
autowhen it improves readability, not by default everywhere. - Prefer brace initialization for variables and objects instead of
=or()where it is valid and readable. - Always use braces for control flow.
- Place member variables at the end of the class definition.
- Keep comments concise and useful; follow the existing Doxygen style in headers.
- Never assume an optimization helps; use
perfor assembly output to validate. - Watch unnecessary allocations, cache locality, vectorization hints, and false sharing.
- Consider the performance impact of every change, especially on the frontend where even a single extra branch can matter. The backend is less sensitive, but throughput still matters.