Files
magnus919_agent-skills/scripts/logging_utils.py
T
Magnus HedemarkandGitHub 6b44d6f490 feat: improve agent readiness with dev tooling, CI checks, and tests
All CI steps pass including the root pyproject.toml build fix.

14 signals addressed across 3 phases:
- Phase 1: single_command_setup, devcontainer, large_file_detection, tech_debt_tracking, duplicate_code_detection
- Phase 2: structured_logging, log_scrubbing, test_isolation, service_flow_documented, agents_md_validation
- Phase 3: integration_tests_exist, automated_security_review, runbooks_documented, issue_labeling_system
2026-07-29 18:23:26 -04:00

117 lines
3.6 KiB
Python

"""Structured logging with PII/sensitive-data redaction.
Uses loguru for structured, colorized output with automatic redaction of
sensitive patterns (API keys, tokens, credentials, email addresses, IPs).
"""
import functools
import os
import re
import sys
from typing import Any
from loguru import logger as _logger # type: ignore[import-not-found,unused-ignore]
# Patterns detected and redacted in log messages
_REDACT_PATTERNS: list[tuple[str, str]] = [
# API keys and tokens (common formats)
(
r"(?i)(api[_-]?key|apikey|secret[_-]?key|auth[_-]?token|access[_-]?token|bearer)\s*[:=]\s*[\S]+",
r"\1=<REDACTED>",
),
# JWT tokens
(r"eyJ[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,}", "<JWT_REDACTED>"),
# GitHub tokens
(r"gh[pousr]_[A-Za-z0-9_]{20,}", "<GITHUB_TOKEN_REDACTED>"),
# Generic hex tokens (32+ hex chars)
(r"\b[a-fA-F0-9]{32,}\b", "<HEX_TOKEN_REDACTED>"),
# Email addresses
(r"\b[\w.+-]+@[\w-]+\.[\w.-]+\b", "<EMAIL_REDACTED>"),
# IPv4 addresses
(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", "<IP_REDACTED>"),
]
def _redact_sensitive(message: str) -> str:
"""Strip sensitive data patterns from a log message."""
for pattern, replacement in _REDACT_PATTERNS:
message = re.sub(pattern, replacement, message)
return message
def _redacting_patcher(record: Any) -> None:
"""Loguru patcher that redacts sensitive data from the message."""
record["message"] = _redact_sensitive(str(record["message"])) # type: ignore[index,unused-ignore]
def configure_logger(
level: str = "INFO",
json_output: bool = False,
log_file: str | None = None,
) -> None:
"""Configure the global logger with sensible defaults.
Args:
level: Minimum log level (DEBUG, INFO, WARNING, ERROR).
json_output: Emit JSON-structured logs instead of colorized text.
log_file: Optional path for file-based logging.
"""
_logger.remove()
# Console sink: colorized for humans, JSON for machines
if json_output:
_logger.add(
sys.stderr,
level=level,
format="{message}",
serialize=True,
)
else:
_logger.add(
sys.stderr,
level=level,
format=(
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
"<level>{level: <8}</level> | "
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
"<level>{message}</level>"
),
colorize=True,
)
# File sink if requested
if log_file:
os.makedirs(os.path.dirname(log_file) or ".", exist_ok=True)
_logger.add(
log_file,
level="DEBUG",
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} - {message}",
rotation="10 MB",
retention="7 days",
serialize=False,
)
# Apply redaction to all messages
_logger.configure(patcher=_redacting_patcher)
def get_logger(name: str = __name__) -> Any:
"""Return a bound logger for the given module name."""
return _logger.bind(name=name)
@functools.wraps(print)
def safe_print(*args: Any, **kwargs: Any) -> None:
"""Print wrapper that redacts sensitive data."""
message = " ".join(str(arg) for arg in args)
kwargs.pop("file", None) # always use stderr/default
_logger.info(message)
# Auto-configure on import in non-production settings
if os.environ.get("LOGURU_LEVEL") or os.environ.get("CI"):
configure_logger(
level=os.environ.get("LOGURU_LEVEL", "INFO"),
json_output=bool(os.environ.get("CI")),
)