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/. - Keep each changelog entry to one or two brief sentences describing a real usage scenario and the observable fix or benefit for library users. Name the relevant public class, function, or option and the conditions that trigger the issue; scope descriptions of hangs, errors, or missing output to that case. Keep internal implementation details in code comments or commit descriptions.
- Do not add changelog entries for documentation-only or benchmark-only changes.
- Match the surrounding changelog formatting: no blank lines between bullet entries, and indent wrapped continuation lines by two spaces.
- 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. - Do not use
SUBCASEin unit tests. Use separate, independently namedTEST_CASEs with their own setup and cleanup so each scenario is readable and runnable on its own. Prefer a little setup duplication to shared scenario-selection flags. - 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 blank lines to separate logical steps or changes of context in code and tests, such as setup, actions, assertions, and cleanup. Keep related statements together; avoid dense walls of text or a blank line after every statement.
- Keep assignments out of
ifandwhileconditions: compute the value first, then test it. Keep state-changing steps separate from compound conditions so their order is easy to follow. - 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.
- Group members by access pattern first: keep frequently accessed members used together adjacent so they can share cache lines, while preserving intentional separation between threads. Within each group, prefer decreasing size, account for alignment, and place small flags together to reduce padding. Preserve initialization and destruction dependencies; verify the compiler's layout before claiming size or cache-line improvements.
- 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.