stogger-systemd¶
Context¶
Services running under systemd produce redundant metadata in journalctl — double timestamps and level indicators because journalctl adds its own header while stogger’s ConsoleFileRenderer adds another. JournalLoggerFactory is a stub returning None, so nothing writes to the journal via journal.send(). Extracting the journal I/O into a separate workspace package with an optional dependency on systemd-python fixes this.
Decisions¶
packaging-model¶
Context¶
The journal I/O needs isolation from core because systemd-python only installs on Linux. The project uses hatch-vcs with src-layout. Users should be able to install a separate package from PyPI.
Decision¶
uv workspace with separate package. Root pyproject.toml stays as stogger package and becomes workspace root via [tool.uv.workspace] members = [“packages/*”]. New package at packages/stogger-systemd/ with its own pyproject.toml. Both packages share VCS version via hatch-vcs with raw-options.root pointing to the git root. Install via pip install stogger-systemd which pulls stogger + systemd-python.
Alternatives¶
a. Optional extra in same distribution (pip install stogger[systemd]) — no package separation, all code in one wheel. Rejected: users expect separate package for separate concern. b. Separate repository — maximum isolation but versioning nightmare for shared VCS. Rejected. c. Inline module with conditional import — no isolation, all users carry the import overhead. Rejected.
Consequences¶
Two independently publishable packages sharing one VCS tag. packages/stogger-systemd/ declares stogger as dependency, uses [tool.uv.sources] stogger = { workspace = true } during development. Platform isolation at dependency level — pip install stogger-systemd fails gracefully on non-Linux via systemd-python resolution.
scope-split¶
Context¶
Core holds journal data transformation (SystemdJournalRenderer, JOURNAL_LEVELS) and a stub logger factory. The real journal I/O needs the systemd-python dependency.
Decision¶
Only the I/O layer moves to packages/stogger-systemd/: JournalLogger (with journal.send()), DummyJournalLogger, and the real JournalLoggerFactory. Renderer, level constants, and syslog import stay in core — pure data transformation, no platform deps.
Alternatives¶
a. Full extraction of renderer + factory — requires plugin mechanism in core for dict formatting. Rejected. b. Everything including syslog constants out — syslog is stdlib on all platforms, renderer useful in tests. Rejected.
Consequences¶
Core keeps ~60 lines of journal formatting. New package is a thin I/O shim (~30 lines). Stub in core remains as fallback.
integration-hook¶
Context¶
Core must discover the real journal logger factory when stogger-systemd is installed. Since it is a separate package, ImportError fires when it is not installed.
Decision¶
Dynamic import with ImportError fallback. Core attempts from stogger_systemd import get_journal_logger_factory. Falls back to stub on ImportError. Zero-config for users.
Alternatives¶
a. Entry point plugin — over-engineering for single integration point. Rejected. b. Explicit import stogger_systemd side-effect — breaks “install and it works” DX. Rejected. c. None-check pattern — only needed when module is always present (same distribution). Not applicable for separate package. Rejected.
Consequences¶
Single try/except block. The import path stogger_systemd.get_journal_logger_factory is the stable contract between packages.
enable-systemd-source¶
Context¶
init_logging() needs to know whether to attempt journal registration. StoggerConfig already has an enable_systemd field read from [tool.stogger] in pyproject.toml.
Decision¶
enable_systemd comes from pyproject.toml config only — no new kwarg on init_logging(). The function reads StoggerConfig internally to get the setting. Convention over configuration: the config file is the source of truth.
Alternatives¶
a. New kwarg enable_systemd: bool = True — adds another parameter to an already long signature. Rejected. b. Accept StoggerConfig object — API break for existing callers. Rejected.
Consequences¶
init_logging() gains a side-effect (reading pyproject.toml via StoggerConfig). Programmatic override requires writing to [tool.stogger] or calling StoggerConfig(enable_systemd=False) separately.
journal-registration-flow¶
Context¶
init_logging() currently builds a loggers dict, then calls structlog.configure(). Journal registration is completely absent. Console suppression under JOURNAL_STREAM exists but is unrelated.
Decision¶
Standalone block after loggers-dict construction, before structlog.configure(). Flow: (1) file logger, (2) console logger with JOURNAL_STREAM suppression, (3) journal logger via dynamic import — independent of console, gated by enable_systemd from config, (4) configure. Console suppression and journal registration are decoupled concerns.
Alternatives¶
a. Journal registration inside if log_to_console block — conflates two concerns. Rejected. b. Separate helper function _try_register_journal() — over-abstraction for one try/except. Rejected.
Consequences¶
Clear control flow. JOURNAL_STREAM detection triggers info message when import fails, but does not gate the import attempt itself.
systemd-facility-plumbing¶
Context¶
StoggerConfig.systemd_facility exists but init_logging() hardcodes syslog.LOG_LOCAL1 in the SystemdJournalRenderer constructor (core.py line 493). LOG_LOCAL1 was fc-agent-specific. The renderer’s own default is LOG_LOCAL0 (line 642).
Decision¶
Fix now. init_logging() reads systemd_facility from StoggerConfig and passes it through to SystemdJournalRenderer unchanged. When config value is None, uses LOG_LOCAL0 (the renderer’s own default). No type conversion — config value is passed as-is.
Alternatives¶
a. Keep LOG_LOCAL1 default — backward compat with fc-agent, but was never a conscious decision. Rejected. b. Convert str to int via getattr(syslog, value) — adds complexity for a field that should match the renderer’s expected type. Rejected.
Consequences¶
Existing users with [tool.stogger] systemd_facility set get correct behavior. Users relying on the hardcoded LOG_LOCAL1 get LOG_LOCAL0 instead — no practical impact since journal filtering rarely distinguishes between LOCAL0/LOCAL1.
fallback-behavior¶
Context¶
Most stogger users don’t need journal integration. Fallback must be graceful.
Decision¶
When JOURNAL_STREAM detected but stogger-systemd not installed (ImportError): one-time info message “systemd journal detected but stogger-systemd not available. Install stogger-systemd package for journal integration.” No package manager mention. Non-systemd: silent skip.
Alternatives¶
a. Silent skip always — NixOS/systemd users have no indication journal is missing. Rejected. b. Hard error — breaks existing setups that never used journal. Rejected.
Consequences¶
One conditional info message when JOURNAL_STREAM is set. Informational, not a warning.
api-contract¶
Context¶
Core needs a stable, minimal interface to import from the extra package.
Decision¶
stogger-systemd exports get_journal_logger_factory() -> JournalLoggerFactory (structlog-compatible factory). Plus JournalLogger and DummyJournalLogger as importable classes.
Alternatives¶
a. Bare JournalLogger class — core would need to instantiate, coupling to constructor. Rejected. b. Whole module as plugin — no explicit contract. Rejected.
Consequences¶
Single integration point. Core: factory = get_journal_logger_factory() then loggers[“journal”] = factory.
test-strategy¶
Context¶
Integration point in core.py must be testable without systemd-python.
Decision¶
Dual test locations. Permanent tests in tests/test_systemd_integration.py — mock stogger_systemd import via unittest.mock.patch. Spec-validation tests in tests/impl_spec/ until Phase 2 makes them green, then cleanup. Test matrix: (1) enable_systemd=True + import succeeds -> journal registered, (2) enable_systemd=True + ImportError -> fallback, (3) enable_systemd=False -> no import attempt, (4) JOURNAL_STREAM + ImportError -> info message.
Alternatives¶
a. Only spec-validation tests — no permanent coverage after cleanup. Rejected. b. Only permanent tests — spec-validation contract not explicitly tracked. Rejected.
Consequences¶
All four integration paths tested without systemd-python dependency.
migration-notice¶
Context¶
Existing stogger users may wonder if this is breaking.
Decision¶
No breaking change. Core never delivered actual journal I/O (stub returned None). At most “New: stogger-systemd package available” note.
Alternatives¶
a. Minor changelog entry — acceptable but not required since nothing removed. Rejected as unnecessary. b. Major version bump — overkill, no behavior changes. Rejected.
Consequences¶
Stogger stays on current version. stogger-systemd is a pure addition.