import logging import logging.config from typing import Any import pydantic import rich import sentry_sdk from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration from sentry_sdk.integrations.httpx import HttpxIntegration from resonance_engine.config import settings from resonance_engine.runtime.types import ( FanCollectDLQResult, FanCollectResult, FanFanoutResult, ) from resonance_engine.tasks.enums import FanoutSource from resonance_engine.tasks.handlers import ( RunCollectDLQRequest, RunCollectRequest, RunFanoutRequest, run_collect, run_collect_dlq, run_fanout, ) from resonance_engine.tasks.types import FanCollectBatch # Force rich to treat output as a terminal so ANSI colours are rendered # in worker processes where stdout/stderr may not be a TTY. if settings.logging_debug: rich.reconfigure(force_terminal=True) # Configure logging logging.config.dictConfig(settings.logging_config) # Setup Sentry if settings.sentry_dsn: sentry_sdk.init( dsn=settings.sentry_dsn, traces_sample_rate=settings.sentry_traces_sample_rate, send_default_pii=settings.sentry_send_default_pii, integrations=[ AwsLambdaIntegration(), HttpxIntegration(), ], ) # Get logger logger = logging.getLogger(__name__) def fan_fanout(_event: dict[str, Any], _context: Any) -> FanFanoutResult: logger.info("fan_fanout: started") response = run_fanout(RunFanoutRequest(source=FanoutSource.scheduled)) return FanFanoutResult( clients=[ { "name": item.client_name.value, "fanout_count": item.plan.fanout_count, "dispatched": len(item.fans), } for item in response.results ] ) def fan_collect(event: dict[str, Any], _context: Any) -> FanCollectResult: records = event.get("Records", []) logger.info("fan_collect: started", extra={"record_count": len(records)}) fans_processed = 0 for record in records: try: batch = FanCollectBatch.model_validate_json(record["body"]) except pydantic.ValidationError as exc: # Malformed body — retrying will never help; discard and move on. logger.exception( "fan_collect: invalid record body, discarding", exc_info=exc ) continue logger.info( "fan_collect: batch received", extra={ "dsp_client_name": batch.dsp_client_name.value, "fanout_task_id": str(batch.fanout_task_id), "fans": len(batch.fans), }, ) outcome = run_collect(RunCollectRequest(batch=batch)) fans_processed += outcome.fans_processed if outcome else 0 return FanCollectResult(fans_processed=fans_processed) def fan_collect_dlq(event: dict[str, Any], _context: Any) -> FanCollectDLQResult: records = event.get("Records", []) logger.info("fan_collect_dlq: started", extra={"record_count": len(records)}) batches_marked_stale = 0 for record in records: try: batch = FanCollectBatch.model_validate_json(record["body"]) except pydantic.ValidationError as exc: logger.exception( "fan_collect_dlq: invalid record body, skipping", exc_info=exc ) continue logger.warning( "fan_collect_dlq: processing DLQ message", extra={ "dsp_client_name": batch.dsp_client_name.value, "fanout_task_id": str(batch.fanout_task_id), "collect_task_id": str(batch.collect_task_id), "fans": len(batch.fans), }, ) run_collect_dlq(RunCollectDLQRequest(batch=batch)) batches_marked_stale += 1 return FanCollectDLQResult(batches_marked_stale=batches_marked_stale) def prepare_key_rotation(_event: dict[str, Any], _context: Any) -> Any: return {} def rotate_keys(_event: dict[str, Any], _context: Any) -> Any: return {}