"""Alarm emitter — best-effort CRITICAL alarm publisher (dcy-033 / REQ-100). Publishes REQ-098 alarm envelopes to the `trex.alerts` exchange via the MessagingTransport port. No pika import here — dependency inversion preserved (NFR-1). Domain layer is never touched. Caller contract: - Inject into execution / reconciliation / killswitch / ledger_outbox / etc. - Call publish_alarm(code, title, fields=...) at each alarm point. - publish_alarm() NEVER re-raises. Alarm failure is ERROR-logged and swallowed so the core business path (slot transition / order execution / ledger) is never blocked by alarm delivery. transport=None is explicitly supported for CLI entry points (e.g. reset.py) where no broker connection exists. In that case publish_alarm() logs and skips. """ from __future__ import annotations import logging from datetime import datetime, timezone, timedelta from typing import Optional from trex_core.application.messaging import MessagingTransport, TransportUnavailableError from trex_core.adapters.messaging.topology import alarm_routing_key _log = logging.getLogger(__name__) _KST = timezone(timedelta(hours=9)) # Wire constants (REQ-098) _SOURCE = "trex-core" _SCHEMA_VERSION = "1" class AlarmEmitter: """Application-layer alarm publisher (dcy-033 / REQ-100). Wraps MessagingTransport.publish_alarm_event() with: - envelope construction (source/severity/occurred_at/schema_version fixed) - exception swallowing + ERROR log on failure - None-transport guard for non-daemon entry points """ def __init__(self, transport: Optional[MessagingTransport]) -> None: self._transport = transport def publish_alarm( self, code: str, title: str, *, fields: Optional[dict] = None, severity: str = "critical", ) -> None: """Publish a single alarm envelope. Best-effort: never re-raises. Args: code: Alarm identifier code (see ALARM-INVENTORY.md §A). title: Human-readable 1-line summary. No credentials or raw exception bodies — callers must sanitize. fields: Optional structured context dict (opaque, default {}). severity: 'critical' | 'warn' | 'info'. Default 'critical'. """ if self._transport is None: _log.info( "alarm skipped (no transport): code=%s title=%s", code, title, ) return routing_key = alarm_routing_key(severity, _SOURCE) occurred_at = datetime.now(tz=_KST).isoformat() payload = { "source": _SOURCE, "severity": severity, "code": code, "title": title, "fields": fields or {}, "occurred_at": occurred_at, "schema_version": _SCHEMA_VERSION, } try: self._transport.publish_alarm_event(routing_key, payload) except TransportUnavailableError as exc: _log.error( "alarm publish skipped (transport unavailable): code=%s: %s", code, exc, ) except Exception: # noqa: BLE001 — alarm must never crash the caller _log.error( "alarm publish failed: code=%s title=%s", code, title, exc_info=True, )