5.1 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||
|---|---|---|---|---|---|---|---|
| architecture | Logging & Run Summary | Structured JSON logging in 10Backward — JsonFormatter, secret redaction, and the startup / error / run_complete event helpers. |
|
2026-08-04T17:51:00Z |
Purpose
tenbackward.logging_setup replaces the scaffold-era basicConfig
text logger with a single-line JSON formatter that downstream
log shippers can index. It also owns the three helpers the runner
uses to record structured events: log_startup, log_error,
log_run_summary.
JsonFormatter
JsonFormatter.format(record) returns one JSON object per log record:
| Key | Source |
|---|---|
ts |
UTC datetime.fromtimestamp(record.created).isoformat(). |
level |
record.levelname (e.g. INFO, ERROR). |
logger |
record.name (always tenbackward). |
message |
record.getMessage(). |
| extras | Any keyword passed via logging.info(..., extra={...}) that is not a reserved LogRecord attribute and whose key is not in _SECRET_KEYS. |
exc_type |
When record.exc_info is set, the exception class name. |
Reserved LogRecord attributes and any key starting with _ are
dropped before serialization, so the payload contains only the values
that callers intentionally attached.
Secret Redaction
_SECRET_KEYS is the canonical denylist applied in two places:
- At format time for keys passed via
extra={...}. - At helper time by
_redact_dict(...)so values reachinglog_startup/log_errornever expose known-sensitive fields even before they hit the formatter.
Deny-list contents (case-insensitive):
token, access_token, mastodon_access_token, password, secret,
authorization, api_key
Any matching key is replaced with the string [REDACTED].
Helper Functions
| Helper | When it fires | Logger / Level | event field |
|---|---|---|---|
configure_json_logging(level=INFO) |
First line of main.main(). Idempotent — clears existing handlers. |
n/a | n/a |
log_startup(**fields) |
After config load + data_dir.mkdir, before the pipeline pass. |
INFO |
startup |
log_error(event, *, exc=None, **fields) |
When a pipeline attempt raises, or when the retry budget is exhausted. | ERROR |
the supplied event name; exc_type added when exc is provided. |
log_run_summary(scanned, matched, posted, skipped, posted_ids) |
Once per successful pass. Silently skipped when all counters are zero and no ids were posted. | INFO |
run_complete |
Startup payload
log_startup is invoked with:
version, site_url, run_at, tz, hashtags, throwback_prefix
Each is added to the startup event payload under its own key.
Run-summary payload
log_run_summary is the canonical "what did the bot do today?" record:
{
"ts": "2026-08-04T09:00:00+00:00",
"level": "INFO",
"logger": "tenbackward",
"message": "run complete",
"event": "run_complete",
"scanned": 0,
"matched": 0,
"posted": 0,
"skipped": 0,
"posted_ids": []
}
A pass with no candidates emits no line at all, matching the "silent on no matches" contract.
Examples
Successful run with one new post
{"ts":"2026-08-04T09:00:00+00:00","level":"INFO","logger":"tenbackward","message":"startup","event":"startup","version":"0.1.0","site_url":"https://blog.example.com","run_at":"09:00","tz":"Europe/Berlin","hashtags":"#throwback,#10backward","throwback_prefix":"Heute vor 10 Jahren:"}
{"ts":"2026-08-04T09:00:01+00:00","level":"INFO","logger":"tenbackward","message":"run complete","event":"run_complete","scanned":1,"matched":1,"posted":1,"skipped":0,"posted_ids":["2025-08-04-post-slug"]}
Retry budget exhausted
{"ts":"...","level":"ERROR","logger":"tenbackward","message":"pipeline_error","event":"pipeline_error","attempt":1,"max_attempts":4,"exc_type":"ConnectionError"}
{"ts":"...","level":"ERROR","logger":"tenbackward","message":"pipeline_error","event":"pipeline_error","attempt":2,"max_attempts":4,"exc_type":"ConnectionError"}
{"ts":"...","level":"ERROR","logger":"tenbackward","message":"pipeline_failed","event":"pipeline_failed","attempts":4,"exc_type":"ConnectionError"}
The container then exits with status 1.
Token accidentally logged
Even if a future call wrote extra={"access_token": "..."}, the
formatter replaces it:
{ "...": "...", "access_token": "[REDACTED]" }