Files
tracer-cloud--opensre/tools/investigation/reporting/delivery/dispatch.py
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

148 lines
5.3 KiB
Python

"""Dispatch rendered reports to every registered delivery adapter.
Each vendor supplies a
:class:`platform.reporting.delivery_registry.ReportDeliveryAdapter` from its
own ``integrations/<vendor>/reporting_adapter.py`` module. The bootstrap
helper in :mod:`tools.investigation.reporting.delivery.bootstrap` triggers
their side-effect registrations before the loop runs, so this dispatch node
never imports from ``integrations.*`` directly (T-4 layering audit, issue
#3352, items 23/28).
Slack is treated as the primary channel — it renders the shared interactive
action blocks the pipeline attaches to Slack messages and updates the
investigation thread's status emoji. All other adapters are called
opportunistically; each decides for itself whether the current
:class:`~core.state.InvestigationState` carries enough context to
deliver.
"""
from __future__ import annotations
import logging
from core.state import InvestigationState
from platform.reporting.delivery_registry import (
ReportDeliveryAdapter,
get_delivery_adapter,
iter_delivery_adapters,
)
from tools.investigation.reporting.delivery.bootstrap import (
ensure_delivery_adapters_registered,
)
from tools.investigation.reporting.formatters.messages import ReportMessages
from tools.investigation.reporting.gitlab_writeback import post_gitlab_mr_writeback
logger = logging.getLogger(__name__)
def dispatch_report(
state: InvestigationState,
messages: ReportMessages,
*,
investigation_id: str | None,
investigation_url: str | None,
) -> list[dict]:
"""Dispatch report messages to every registered delivery channel.
Returns the Slack blocks sent (including the investigation action blocks)
so upstream tests can assert the shared block payload without probing each
vendor separately.
"""
ensure_delivery_adapters_registered()
all_blocks = messages.slack_blocks + _build_slack_action_blocks(
investigation_url or "", investigation_id
)
resolved = state.get("resolved_integrations") or {}
if isinstance(resolved, dict):
_log_discord_debug(resolved.get("discord", {}))
payload = _messages_payload(messages)
slack_adapter = get_delivery_adapter("slack")
if slack_adapter is not None:
_run_adapter(slack_adapter, state, messages=payload, blocks=all_blocks)
for adapter in iter_delivery_adapters():
if adapter.name == "slack":
continue
_run_adapter(adapter, state, messages=payload, blocks=all_blocks)
post_gitlab_mr_writeback(state, messages.slack_text)
return all_blocks
def _run_adapter(
adapter: ReportDeliveryAdapter,
state: InvestigationState,
*,
messages: dict[str, object],
blocks: list[dict],
) -> None:
"""Invoke ``adapter.deliver`` and swallow non-fatal errors.
Vendor adapters raise ``RuntimeError`` only when the failure must abort
the investigation (e.g. the Slack thread that triggered the run failed to
receive the report). Anything else is logged and does not stop other
adapters from running.
"""
try:
adapter.deliver(state, messages=messages, blocks=blocks)
except RuntimeError:
# Fail-closed adapters (currently Slack) raise so upstream can surface
# the error; do not swallow.
raise
except Exception: # noqa: BLE001
logger.warning(
"[publish] %s adapter raised while delivering report",
adapter.name,
exc_info=True,
)
def _messages_payload(messages: ReportMessages) -> dict[str, object]:
"""Return a plain ``dict`` view of the rendered per-channel messages.
Adapters accept a :class:`platform.reporting.delivery_registry.DeliveryContext`
(a ``Mapping``) so they never touch the concrete
:class:`~tools.investigation.reporting.formatters.messages.ReportMessages`
dataclass — that keeps the platform boundary free of ``tools`` types.
"""
return {
"slack_text": messages.slack_text,
"slack_blocks": messages.slack_blocks,
"telegram_html": messages.telegram_html,
"whatsapp_text": messages.whatsapp_text,
"sms_text": messages.sms_text,
}
def _log_discord_debug(discord_creds: object) -> None:
if isinstance(discord_creds, dict):
logger.debug(
"[publish] discord creds present=%s keys=%s",
bool(discord_creds),
list(discord_creds.keys()),
)
else:
logger.debug("[publish] discord creds present=%s", bool(discord_creds))
def _build_slack_action_blocks(investigation_url: str, investigation_id: str | None) -> list[dict]:
"""Build the shared Slack action blocks without importing ``integrations.slack``.
The Slack adapter is the canonical source for these blocks, but importing
it directly from the dispatch node reintroduces the ``tools ->
integrations`` edge we're removing. Instead, we ask the registered Slack
adapter to build them: it exposes ``build_action_blocks`` as a plain
attribute, and adapters that do not implement it (test stubs, non-Slack
vendors) simply skip the enrichment.
"""
slack_adapter = get_delivery_adapter("slack")
builder = getattr(slack_adapter, "build_action_blocks", None)
if builder is None:
return []
result = builder(investigation_url, investigation_id)
return list(result) if isinstance(result, list) else []