# 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.

## Verified By

<!-- Tests will be added after implementation -->
