Skip to content

[M0-CORE-02] Structured logging facade - #2

Merged
offdev merged 2 commits into
masterfrom
m0-core-02-logging
Sep 10, 2026
Merged

offdev merged 2 commits into
masterfrom
m0-core-02-logging

Conversation

@offdev

@offdev offdev commented Sep 10, 2026

Copy link
Copy Markdown
Owner

M0-CORE-02 from roadmap/M0-foundations.md: the one structured logging facade (AGENTS §14) in laige-core.

  • LAIGE_LOG_* macros: level gate before argument evaluation — a disabled event is one atomic load + branch, no allocation (LOG-003, verified: 100k disabled Trace events → 0 allocations).
  • Per-subsystem level table + atomic global minimum; stable subsystem/event names; locale-free scalar field rendering.
  • Rate limiting per (subsystem, event, severity) for Warn/Error/Fatal with 'rate_limited' suppressed-count summaries; pending counts drained at shutdown (LOG-004).
  • ConsoleSink (non-owning stream) + FileSink (owning FILE*, create() → Result, failure = new ErrorCode::IoError 5) with the LOG-007 console fallback.
  • Fatal = emit + flush + std::abort(); crash handlers (POSIX sigaction / Windows vectored SEH) flush allocation-free; idempotent shutdown (CONC-006).
  • Additive M0-CORE-01 extensions: Result::takeValue() && and IoError (both tested in result_status).
  • docs/api/logging.md (AGENTS §9 contract); roadmap board/changelog/decision notes in the same commit.

Verified locally: GCC static/shared/ASan/TSan + Clang static/shared — ctest 8/8 and ctest -R logging green in every tree, zero warnings (disabled ≈46 ns/event vs ≈1207 ns/event enabled).

The one structured logging facade for the engine (AGENTS §14
LOG-001…LOG-007, FR-12.2): laige-core gains include/laige/logging.h
and logging.cpp.

Facade: laige::log::Logger — a process-lifetime Meyers singleton that
owns its current Sink (unique_ptr). LAIGE_LOG_TRACE/DEBUG/INFO/
WARN/ERROR/FATAL macros put the level gate before argument evaluation,
so a disabled event costs exactly one atomic load + branch: no
message/field evaluation, no formatting, no allocation, no lock
(LOG-003). Gating is an atomic global minimum plus a per-subsystem
level table (init-phase mutation, one facade mutex, CONC-001). Fields:
scalars render locale-free via std::to_chars into a 64-byte stack
buffer; string-like values copy in full; field values must avoid
spaces/'=' (the line is '| k=v' machine-parseable, LOG-001).

Rate limiting (LOG-004): per (subsystem, event, severity) for
Warn/Error/Fatal only. The first event of a key is always recorded
(an everEmitted flag — a time_point::min() sentinel overflowed the
window subtraction, CPP-004); repeats are counted and reported as a
stable 'rate_limited' summary event (event=<orig>, suppressed=N) at
window rollover, and pending counts are drained at shutdown.

Sinks: ConsoleSink (does not own the stream — stderr must outlive the
process) and FileSink (owns the FILE*; create() returns
Result<unique_ptr<FileSink>>, open failure = ErrorCode::IoError, the
new slot-5 registry code; the LOG-007 minimal fallback keeps the
console sink and reports the failure). Fatal records, flushes, and
terminates via std::abort() (AGENTS §14 controlled termination).
Crash handling: POSIX sigaction (SA_RESETHAND one-shot) for
SIGSEGV/ABRT/BUS/FPE/ILL and a vectored SEH handler on Windows — raw
write(2) notice, allocation-free try_lock sink flush, re-raise.
shutdown() is idempotent (CONC-006): drain summaries, flush, retire
(post-shutdown logs discarded). Timestamps: system_clock, UTC RFC
3339, rendered with an in-code Hinnant civil-from-days (no libc date
functions).

Additive M0-CORE-01 extensions required by this step's FileSink→facade
hand-off: Result::takeValue() && (move the success value out of an
rvalue result) and ErrorCode::IoError (5), both covered in the
result_status suite; docs/api/errors.md documents the new code.

Tests: 'logging' CTest entry (27 GTest cases in the shared
laige-core_tests executable) — gate semantics, field formatting, both
sinks (exact line format, dtor flush, create failure → IoError), rate
limiting (suppression, summary after window, window edge, key
independence, Warn/Error/Fatal only, opt-out, shutdown drain), Fatal
(gate + forked child that emits, flushes, and dies on SIGABRT with the
line in the file), crash/shutdown behavior, concurrency (4 threads ×
250), and LogPerformance: 100k disabled Trace events → 0 allocations
via a test-only global operator new counter (excluded from the
sanitizer trees, whose runtimes define their own new/delete — there
the same spam loop runs leak-free plus the timing property, the
fallback the step names). Measured: disabled ≈46 ns/event vs
≈1207 ns/event enabled (N=200000).

Verified locally 2026-09-10 (GCC 16.2.1 static, shared, ASan/UBSan,
TSan trees; fresh Clang 22.1.8 static + shared trees): ctest 8/8 and
ctest -R logging green in every tree, zero warnings.

API contract: docs/api/logging.md. Roadmap: M0-CORE-02 checked with
Decision/Verify/Size notes; Progress Board M0 11/22; change log line.
… jobs)

ci-pull.yml run 34534697621 on 0d3ee28 (PR #2): linux-gcc (g++),
linux-clang, linux-asan+UBSan (clang++), linux-tsan (clang++), and
include-lint all passed in 50 s; macOS/Windows jobs skipped
(label-gated, default lane is Linux). Change-log hash filled in;
roadmap Verify note updated.
@offdev
offdev merged commit c1df233 into master Sep 10, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant