fix: logging overhaul — Alembic fileConfig disable_existing_loggers=False, propagate=False on loggers, ADR docs, catch-up migration for missing columns, sqlparse dep, build archive naming

This commit is contained in:
2026-05-27 11:19:12 +03:00
parent 614bf9123a
commit 143ab15744
5 changed files with 109 additions and 17 deletions

View File

@@ -6,9 +6,20 @@
# @RELATION DEPENDS_ON -> [WebSocketLogHandler]
# @PRE Python 3.7+ with cot_logger ContextVars available.
# @POST All log output is single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id.
# @SIDE_EFFECT Configures global logger handlers and formatters; seeds global _enable_belief_state and _task_log_level; writes JSON to console/files.
# @RATIONALE Replaced BeliefFormatter text-based [Entry]/[Exit]/[Action] logging with CotJsonFormatter JSON output. Old format was not machine-parseable and mixed presentation with intent. CotJsonFormatter produces structured JSON consistent with cot_logger.py MarkerLogger protocol.
# @REJECTED Keeping both formatters side-by-side was rejected (two inconsistent output formats would confuse log consumers).
# Both 'superset_tools_app' and 'cot' loggers have propagate=False — no text duplicates via root logger.
# @INVARIANT Every logger.info() call MUST produce exactly one JSON line (see [ADR LOG-002] for why).
# @SIDE_EFFECT Configures global logger handlers and formatters; seeds global _enable_belief_state
# and _task_log_level; sets propagate=False on both loggers; writes JSON to console/files.
# @RELATION DISABLED_BY -> [LOG-001:alembic/env.py:fileConfig]
# If alembic/env.py calls fileConfig() without disable_existing_loggers=False, Python 3.13's
# logging framework sets logger.disabled = True on 'superset_tools_app', silencing ALL output.
# @RATIONALE Replaced BeliefFormatter text-based [Entry]/[Exit]/[Action] logging with CotJsonFormatter
# JSON output. Old format was not machine-parseable and mixed presentation with intent. CotJsonFormatter
# produces structured JSON consistent with cot_logger.py MarkerLogger protocol.
# @REJECTED Keeping both formatters side-by-side was rejected (two inconsistent output formats would
# confuse log consumers).
# @REJECTED Keeping propagate=True was rejected after uvicorn's dictConfig added a root logger handler
# that duplicates every log as plain text (see [ADR LOG-002]).
# @DATA_CONTRACT All log records conform to molecular CoT JSON schema: {ts, level, trace_id, src, marker, intent, span_id?, payload?, error?}
# @INVARIANT CotJsonFormatter.format() always returns valid single-line JSON string.
from contextlib import contextmanager
@@ -162,8 +173,12 @@ def belief_scope(anchor_id: str, message: str = ""):
# @RELATION CALLS -> [CotJsonFormatter]
# @RELATION CALLS -> [CotLoggerModule]
# @PRE config is a valid LoggingConfig instance.
# @POST Logger levels, handlers, formatters, belief state flag, and task log level are updated.
# @SIDE_EFFECT Mutates global _enable_belief_state, _task_log_level; adds/removes handlers on both logger and cot_logger; creates file directories.
# @POST Logger levels, handlers, formatters, belief state flag, task log level, and
# propagate flags (False) are updated. Both loggers emit JSON only — no text duplicates.
# @SIDE_EFFECT Mutates global _enable_belief_state, _task_log_level; adds/removes handlers
# on both logger and cot_logger; sets propagate=False on both; creates file directories.
# @DATA_CONTRACT configure_logger() -> ENABLES both loggers. Every call to logger.info()
# MUST produce exactly one JSON line (not zero, not two). See [ADR LOG-001] and [ADR LOG-002].
def configure_logger(config):
global _enable_belief_state, _task_log_level
_enable_belief_state = config.enable_belief_state
@@ -206,9 +221,21 @@ def configure_logger(config):
cot_handler.setFormatter(CotJsonFormatter())
cot_logger.addHandler(cot_handler)
# Disable propagation to prevent duplicate output via uvicorn's root logger handlers.
# Both loggers have their own dedicated handlers; propagation would cause each message
# to appear twice: once via CotJsonFormatter (JSON) and once via uvicorn's DefaultFormatter (text).
# @ADR [LOG-002] Disable logger propagation to prevent duplicate output
# @RATIONALE Uvicorn's configure_logging() registers a StreamHandler on the root
# logger via logging.config.dictConfig(). Both 'superset_tools_app' and 'cot'
# loggers have their own dedicated handlers with CotJsonFormatter. With
# propagate=True (default), each log message is handled twice:
# 1. Our CotJsonFormatter → structured JSON (desired)
# 2. Propagated to root → uvicorn's DefaultFormatter → plain text (duplicate)
# Setting propagate=False prevents the second emission.
# @REJECTED Removing uvicorn's root logger handler via logging.getLogger().handlers = []
# was rejected because it couples SS-TOOLS to uvicorn's internal logging setup,
# which could change between uvicorn versions. Setting propagate on our own
# loggers is self-contained and version-safe.
# @REJECTED Overriding log level on root logger was rejected (root.setLevel(WARNING))
# because it would suppress legitimate warnings from third-party libraries
# (SQLAlchemy, Alembic, httpx, etc.).
logger.propagate = False
cot_logger.propagate = False
# #endregion configure_logger