Python Logging Best Practices
Python logging with loguru, structlog, and orjson. TRIGGERS - loguru, structlog, structured logging
- Skill ID
- terrylica/cc-skills/python-logging-best-practices
- Publisher
- terrylica
- Repository
- cc-skills
- Installs
- 0
- Files
- 5
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
terrylica/cc-skills/python-logging-best-practicesInstalls these files- SKILL.md
- references/evolution-log.md
- references/logging-architecture.md
- references/loguru-patterns.md
- references/migration-guide.md
What this skill tells the agent
Python Logging Best Practices
Self-Evolving Skill: This skill improves through use. If instructions are wrong, parameters drifted, or a workaround was needed — fix this file immediately, don't defer. Only update for real, reproducible issues.
When to Use This Skill
Use this skill when:
- Setting up Python logging for any service or script
- Configuring structured JSONL logging for analysis
- Implementing log rotation
- Choosing between lightweight (zero-dep) and full-featured logging
- Adding logging to containerized, systemd, or local applications
Overview
Unified reference for Python logging patterns optimized for machine readability (Claude Code analysis) and operational reliability. Starts with the lightest viable approach and scales up only when needed.
Decision Heuristic: Start Light, Scale Up
Is it < 5 services on a single machine, < 1 event/sec?
YES → Lightweight Pattern (print + JSONL telemetry)
NO → Is it containerized / serverless?
YES → stdout JSON (any library), no file rotation
NO → Is OTel tracing required?
YES → structlog + OTel
NO → loguru (CLI tools) or stdlib RotatingFileHandler| Approach | Use Case | Pros | Cons |
|---|---|---|---|
| Lightweight | Small systemd services, self-hosted, single operator | Zero deps, journald integration, minimal code | No severity filtering, no per-module control |
loguru | CLI tools, scripts, local services | Zero-config, built-in rotation, great DX | External dep, not truly schema-enforced |
structlog | Production services, OTel integration | ContextVars, processor chains, OTel-native | Steeper learning curve |
stdlib | LaunchAgent daemons, zero-dep constraint | No dependencies, Python 3.14 merge_extra | More boilerplate, no structured defaults |
Logfire | AI/LLM observability, Pydantic apps | Built on OTel, token/cost tracking, SQL | SaaS dependency, newer ecosystem |
Preferred: Lightweight Pattern (Zero Dependencies)
For: < 5 systemd services, single server, single operator. Battle-tested in production by [ccmax-monitor](https://github.com/terrylica/ccmax-monitor).
This pattern uses a two-channel architecture:
- Channel 1:
print(flush=True)→ systemd journald (operational logs, human-readable) - Channel 2: Append-only JSONL file (structured telemetry, machine-readable)
This maps to the 12-Factor App's "treat logs as event streams" principle. journald handles ops (rotation, filtering, metadata), while the JSONL file serves domain telemetry for post-mortem analysis.
Architecture: Three-Concern Separation
| Concern | Mechanism | Purpose | Lifecycle |
|---|---|---|---|
| Ops logging | print() → journald | Human debugging, journalctl -u service -f | Managed by journald (auto-rotated) |
| Telemetry | JSONL file (telemetry.jsonl) | Structured audit trail, AI/LLM analysis | Append-only, rotated by size |
| State recovery | WAL file (optional) | Crash recovery for irreversible operations | Ephemeral, deleted on success |
Complete Lightweight Example
"""Append-only JSONL telemetry logger with size-based rotation.
Zero external dependencies. Works with systemd journald for ops logging
and a separate JSONL file for structured machine-readable telemetry.
"""
import json
from datetime import datetime, timezone
from pathlib import Path
TELEMETRY_PATH = Path(__file__).parent / "telemetry.jsonl"
MAX_SIZE = 10 * 1024 * 1024 # 10 MB
BACKUP_COUNT = 3 # Keep 3 rotated backups (~30MB total)
def log_event(event_type: str, data: dict) -> None:
"""Append a structured JSON line to telemetry.jsonl."""
entry = {
"ts": datetime.now(timezone.utc).isoformat(),
"type": event_type,
**data,
}
line = json.dumps(entry, separators=(",", ":")) + "\n"
try:
try:
if TELEMETRY_PATH.stat().st_size > MAX_SIZE:
_rotate()
except FileNotFoundError:
pass
with open(TELEMETRY_PATH, "a") as f:
f.write(line)
except OSError as e:
# Fallback to stderr (captured by journald)
print(f"[telemetry] write failed: {e}", file=__import__("sys").stderr, flush=True)
def _rotate() -> None:
"""Rotate telemetry files: .jsonl → .jsonl.1 → .jsonl.2 → .jsonl.3"""
for i in range(BACKUP_COUNT, 1, -1):
src = TELEMETRY_PATH.with_suffix(f".jsonl.{i - 1}")
dst = TELEMETRY_PATH.with_suffix(f".jsonl.{i}")
if src.exists():
dst.unlink(missing_ok=True)
src.rename(dst)
backup = TELEMETRY_PATH.with_suffix(".jsonl.1")
backup.unlink(missing_ok=True)
TELEMETRY_PATH.rename(backup)
# === Ops logging (goes to journald via stdout) ===
def log(msg: str) -> None:
"""Human-readable operational log line. Captured by journald."""
ts = datetime.now(timezone.utc).strftime("%H:%M:%S")
print(f"[{ts}] {msg}", flush=True)Usage:
# Operational (human reads via journalctl -u myservice -f)
log("Refreshing token for account X")
log("Switch: account A → account B (reason: 5h breach)")
# Telemetry (machine reads via jq/DuckDB/Claude Code)
log_event("token_refresh", {"account": "X", "expires_in_h": 8.0, "token_fp": "abc12345"})
log_event("account_switch", {"from": "A", "to": "B", "reason": "5h_breach"})Security: Token Fingerprinting (Not Regex Redaction)
Never pass secrets through the logging pipeline. Log only a non-reversible fragment:
def _token_fingerprint(token: str) -> str:
"""Extract uniquely identifiable chars from a token's mid-section.
The prefix (sk-ant-oat01-) and suffix (...AA) are common across tokens.
Chars 14-22 (after the prefix) are the most unique per-token.
Middle-slice avoids leaking type-prefix metadata that prefix-based
approaches expose.
"""
if len(token) > 25:
return token[14:22]
return token[:8] if token else ""
# Usage: log the fingerprint, never the token
log_event("token_refresh", {"account": name, "token_fp": _token_fingerprint(token)})Why this is superior to regex redaction filters:
| Approach | Security | Maintenance | Failure mode |
|---|---|---|---|
| Token fingerprinting (log only a slice) | Secret never enters logging pipeline | Zero — works with any token format | Cannot fail — nothing to redact |
| Regex redaction filter | Secret passes through, filtered on output | Must update regexes for new token formats | Silent miss = secret in logs |
This aligns with OWASP Logging Cheat Sheet: "Ensure that no sensitive data is included in log entries." Major platforms (AWS, Stripe, GitHub) use separate non-secret identifiers or partial token display — never full tokens with regex scrubbing.
Regex filters remain useful as a defense-in-depth backstop, not a primary control.
Health Endpoints as Observability
For small deployments, rich JSON health endpoints replace log aggregation:
More from terrylica
Use this skill in a project
Open any RiverX project, search the Skills panel for terrylica/cc-skills/python-logging-best-practices, and install it in one click.
