postgres-target¶
Context¶
Stogger logs to console, file, and systemd journal but has no database target. PostgreSQL as a logging target makes logs queryable, analysable, and persistent — especially useful for services already running PostgreSQL. The new target follows the established external-package pattern (mirroring stogger-systemd) to keep the core dependency-free.
Decisions¶
package-placement¶
Context¶
PostgreSQL requires a heavy native dependency. Stogger core is deliberately free of I/O-heavy dependencies. The stogger-systemd package establishes the precedent: external package as workspace member, discovered at runtime via dynamic import.
Decision¶
External package stogger-postgres as workspace member under packages/. The renderer (PostgresRenderer) lives in core stogger (like SystemdJournalRenderer). The logger/factory (PostgresLogger, PostgresLoggerFactory) lives in the external package.
Alternatives¶
a. Built into stogger core — forces all users to install psycopg
b. Optional extra behind [postgres] — breaks with established pattern
Consequences¶
Clean separation. Core stays light. Users who want PostgreSQL install stogger-postgres. Runtime dynamic import mirrors journal pattern exactly.
postgres-driver¶
Context¶
The sync-per-event write pattern is decided (see write-pattern). The driver must support synchronous writes, have a pure-Python fallback for environments without a C compiler, and be actively maintained.
Decision¶
psycopg v3 (psycopg). Modern API, pure-Python fallback via psycopg[pure], pipeline mode for future batched writes, COPY support for bulk operations. Declared as dependency in packages/stogger-postgres/pyproject.toml.
Alternatives¶
a. psycopg2 — legacy, C-extension only, difficult on some platforms b. pg8000 — pure-Python but less widely adopted, fewer features
Consequences¶
Standard modern choice. Pure-Python fallback ensures installability everywhere. Pipeline mode available if write-pattern evolves to batched.
schema-columns¶
Context¶
Events have known high-cardinality fields (timestamp, level, event, func, scope) plus arbitrary user-defined fields. The schema must balance query performance on known fields with flexibility for unknown fields.
Decision¶
Fixed columns for high-query-volume fields + JSONB catch-all:
Column |
Type |
Source |
|---|---|---|
|
BIGSERIAL PRIMARY KEY |
auto |
|
TIMESTAMPTZ NOT NULL |
event_dict |
|
TEXT NOT NULL |
event_dict |
|
TEXT NOT NULL |
event_dict |
|
TEXT |
event_dict |
|
TEXT |
event_dict |
|
JSONB NOT NULL DEFAULT ‘{}’ |
all remaining event_dict fields |
Indexes: timestamp (DESC), level, event. GIN index on data.
Alternatives¶
a. Minimal (id, timestamp, level, event + JSONB) — func/scope require JSONB queries b. Fully configurable schema — no out-of-the-box experience
Consequences¶
Common query dimensions are real columns with indexes. func and scope as separate columns enable efficient filtering by decorated function or scope name. Arbitrary fields remain queryable via JSONB.
data-pipeline¶
Context¶
The renderer transforms event_dict for the target. The logger performs I/O. This separation is established by SystemdJournalRenderer (transforms to journal fields) → JournalLogger.msg(dict) (calls journal.send).
Decision¶
PostgresRenderer (in core stogger) extracts known fields into column dict, packs remaining fields into JSONB data, returns {"postgres": column_dict}. PostgresLogger.msg(column_dict) (in external package) executes the INSERT. DummyPostgresLogger is the no-op fallback.
Renderer responsibility: field extraction, column mapping, JSONB packing. Logger responsibility: connection management, schema creation, INSERT execution.
Alternatives¶
a. Raw pass-through — logger does transformation + INSERT, renderer is dummy. Violates renderer/logger separation. b. SQL string — renderer produces INSERT statement. Couples renderer to table schema directly.
Consequences¶
Consistent with established structlog patterns. Renderer is testable without database. Logger is thin I/O layer.
connection-config¶
Context¶
Users must configure the database connection. Socket/peer authentication is common (no password needed). When passwords are needed, they must not be committed to version control.
Decision¶
DSN in pyproject.toml under [tool.stogger] key postgres_dsn. Password optional via STOGGER_POSTGRES_PASSWORD environment variable. Placeholder %PASSWORD% in DSN is replaced at runtime. Socket auth: DSN without placeholder works directly (e.g. postgresql://stogger:@/logs?host=/var/run/postgresql).
Config keys added to StoggerConfig:
enable_postgres: bool = Falsepostgres_dsn: str | None = Nonepostgres_table: str = "stogger_logs"
Alternatives¶
a. Full DSN from environment variable only — works but DSN must be entirely in ENV b. Separate config keys per connection parameter — more config overhead
Consequences¶
DSN can be safely versioned. Password never in code. Socket auth has zero extra complexity.
error-strategy¶
Context¶
Logging must not crash the application. If a target fails, other targets continue.
Decision¶
Silent fallback at every failure point: connection failure, schema creation failure, INSERT failure. Each logs a warning to stderr (not to structlog — avoids recursive logging). Target is skipped, other targets continue. Mirrors the journal fallback pattern in _build_logger_factories().
Alternatives¶
a. Buffer and retry — memory leak risk during extended outages b. Fail hard — crashes application when database is down
Consequences¶
Robust under database outages. Users see warnings, logs flow to working targets.
schema-creation¶
Context¶
The table must exist before INSERTs. Users should not need manual DDL steps.
Decision¶
CREATE TABLE IF NOT EXISTS executed in PostgresLoggerFactory.__call__() — once per logger instantiation, at startup. If creation fails, DummyPostgresLogger is returned (no-op fallback).
Alternatives¶
a. Lazy creation on first INSERT — delays error detection b. Explicit setup function — worse DX, user must remember to call it
Consequences¶
Table is guaranteed to exist before any INSERT. Errors surface immediately at startup. No manual setup required.
write-pattern¶
Context¶
Logging happens in the hot path. The target must not noticeably slow down the application.
Decision¶
Synchronous INSERT per event. Each PostgresLogger.msg(dict) executes one INSERT and returns. Overhead ~1-2ms per event.
Alternatives¶
a. Batched writes — higher throughput but more complex, delayed delivery b. Async background writer — highest throughput but most complex
Consequences¶
Low latency per event. Predictable behaviour. For very high volume (>1000 events/s), batched writes may be needed in future.
test-strategy¶
Context¶
Tests must cover the target without requiring a running PostgreSQL instance in CI. The stogger-systemd package establishes the test pattern.
Decision¶
Mirror the systemd test pattern:
Mock-based integration tests (in stogger core): 4-path decision matrix testing
enable_postgres× import success × env var presence. Usespatch.dict(sys.modules, ...)to mockstogger_postgres.Real-package tests (in
packages/stogger-postgres/): guarded bypytest.importorskip("stogger_postgres")and@pytest.mark.integration. Testsget_postgres_logger_factory(),PostgresLoggerFactory.__call__(),DummyPostgresLogger.msg().Spec validation tests: in
tests/impl_spec/test_postgres_target.pywith xfail markers. Test import paths, config keys, renderer contract, schema creation flow.
Alternatives¶
a. Spec validation tests only — less confidence b. No automated tests — unacceptable for new target
Consequences¶
Full coverage of the registration flow without PostgreSQL in CI. Real-package tests available for local development with a running database.