ADR-004: Logging Strategy¶
Status¶
Accepted Date: 2026-02-14
Context¶
All cosalette applications run as unattended daemons, typically deployed via Docker
containers or systemd services. Logs must be parseable by container log
aggregators (Loki, Elasticsearch, CloudWatch) for centralised monitoring, while
remaining human-readable during local development. The velux2mqtt reference
implementation already includes a custom JsonFormatter (105 lines) that emits NDJSON
with UTC timestamps and correlation metadata (service, version).
Key requirements:
- Container orchestrators (Docker, systemd-journal) parse structured output more effectively than free-text
- Cross-timezone deployments require unambiguous timestamps
- Future centralised log aggregation (tooling undecided) requires consistent format across all 8+ projects
- Local development needs human-readable output
Decision¶
Use JSON (NDJSON) for production and text for development, with UTC timestamps and a parameterised service name, because structured logs are universally parseable by log aggregators and UTC removes timezone ambiguity across distributed deployments.
The framework provides a JsonFormatter that emits one JSON object per log record:
{
"timestamp": "2026-02-14T12:34:56+00:00",
"level": "INFO",
"logger": "velux2mqtt.app",
"message": "Device blind started",
"service": "velux2mqtt",
"version": "0.1.0"
}
The format is selectable via configuration (logging.format = "json" | "text") and CLI
(--log-format). The service field is set from app.name, enabling log correlation
across multiple deployed applications.
Design choices¶
- Custom formatter over
python-json-logger: Zero additional dependencies. Full control over the output schema. Field names match the project's conventions. - UTC timestamps (RFC 3339 / ISO 8601): Container logs cross timezone boundaries. UTC removes ambiguity — display layers apply local time when needed.
- NDJSON format: Each log line is a complete JSON object with no embedded newlines.
Critical for container log drivers that split on
\n. - Correlation metadata:
serviceandversionfields in every log line enable filtering by application and version in aggregators without extra configuration.
Decision Drivers¶
- The Twelve-Factor App methodology (XI. Logs): treat logs as event streams
- Container log drivers require single-line, structured output
- Cross-timezone portability eliminates local time ambiguity
- Future central log aggregation requires consistent, machine-parseable format
- Development convenience requires human-readable alternative
Considered Options¶
Option 1: structlog¶
Use the structlog library for structured logging with processors.
- Advantages: Rich processor pipeline, context binding, beautiful development output.
- Disadvantages: Additional dependency with a significant API surface. The custom
JsonFormatteris only 105 lines and provides exactly what's needed. structlog's processor model adds complexity that isn't justified for the use case.
Option 2: Plain text only¶
Use Python's default logging.Formatter with timestamped text lines.
- Advantages: Zero custom code, human-readable by default.
- Disadvantages: Not parseable by log aggregators without custom regex patterns. No correlation metadata. Breaks with multi-line log messages (exceptions).
Option 3: Syslog¶
Use syslog for log routing via the OS.
- Advantages: OS-level log management, well-established in server environments.
- Disadvantages: Not portable across Docker and bare-metal deployments. Adds infrastructure dependency. Not idiomatic for container-based applications.
Option 4: Dual-format (JSON + text) with custom formatter (chosen)¶
A custom JsonFormatter for production (NDJSON) and standard text formatting for
development, selectable via configuration.
- Advantages: Custom formatter is only ~105 lines with zero dependencies. NDJSON is universally parseable. UTC timestamps for cross-timezone consistency. Parameterised service name enables log correlation. Development mode uses readable text.
- Disadvantages: Custom code to maintain (albeit small). Must ensure the JSON schema remains stable across framework versions.
Decision Matrix¶
| Criterion | structlog | Plain Text | Syslog | Dual-Format Custom |
|---|---|---|---|---|
| Machine parseability | 5 | 1 | 3 | 5 |
| Dev experience | 5 | 4 | 2 | 4 |
| Dependency footprint | 2 | 5 | 4 | 5 |
| Cross-timezone | 4 | 2 | 3 | 5 |
| Aggregator compatibility | 5 | 2 | 3 | 5 |
Scale: 1 (poor) to 5 (excellent)
Consequences¶
Positive¶
- All 8+ projects emit identically structured logs — a single aggregator configuration works for the entire fleet
- UTC timestamps eliminate timezone ambiguity across distributed deployments
serviceandversionfields enable filtering and grouping without extra log pipeline configuration- Developers can switch to human-readable text output with
--log-format text - Zero additional dependencies — the formatter uses only Python stdlib (
json,logging,datetime)
Negative¶
- Custom
JsonFormatteris project-maintained code (~105 lines) rather than a community-maintained library - The JSON schema becomes a contract — field names and structure must remain stable to avoid breaking log pipeline configurations
- Text mode output does not include correlation metadata (
service,version), reducing its usefulness in production
2026-02-14