Files
tracer-cloud--opensre/tools/investigation/reporting/formatters/evidence.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

333 lines
12 KiB
Python

"""Evidence formatting and citation for RCA reports."""
import html
from collections.abc import Callable
from typing import Any
from tools.investigation.reporting.context import ReportContext
from tools.investigation.reporting.formatters.base import (
format_html_link,
format_slack_link,
shorten_text,
)
from tools.investigation.reporting.urls.aws import (
build_datadog_logs_url,
build_grafana_explore_url,
)
def _format_tool_calls_line(
ctx: ReportContext,
*,
link_fn: Callable[[str, str | None], str] = format_slack_link,
) -> str:
"""Summarize tool calls made during investigation from executed_hypotheses.
Returns a compact line like: "Queries: cloudwatch logs (12 events), <Grafana Loki|url> (5 logs)"
Includes deep links for Grafana and Datadog where endpoints are available.
Returns empty string if nothing was executed.
"""
executed_hypotheses = ctx.get("executed_hypotheses", []) or []
if not executed_hypotheses:
return ""
# Collect all action names across all hypothesis rounds (deduped, order preserved)
all_actions: list[str] = []
for hyp in executed_hypotheses:
for action in hyp.get("actions", []):
if action not in all_actions:
all_actions.append(action)
if not all_actions:
return ""
evidence = ctx.get("evidence", {}) or {}
grafana_endpoint = ctx.get("grafana_endpoint") or ""
datadog_site = ctx.get("datadog_site") or "datadoghq.com"
def _grafana_logs_count(e: dict) -> str | None:
logs = e.get("grafana_logs", [])
errors = e.get("grafana_error_logs", [])
if not logs and not errors:
return None
parts = []
if logs:
parts.append(f"{len(logs)} logs")
if errors:
parts.append(f"{len(errors)} errors")
return ", ".join(parts)
def _datadog_logs_count(e: dict) -> str | None:
logs = e.get("datadog_logs", [])
errors = e.get("datadog_error_logs", [])
if not logs and not errors:
return None
parts = []
if logs:
parts.append(f"{len(logs)} logs")
if errors:
parts.append(f"{len(errors)} errors")
return ", ".join(parts)
def _datadog_investigate_count(e: dict) -> str | None:
logs = e.get("datadog_logs", [])
errors = e.get("datadog_error_logs", [])
monitors = e.get("datadog_monitors", [])
events = e.get("datadog_events", [])
if not logs and not errors and not monitors and not events:
return None
parts = []
if logs:
parts.append(f"{len(logs)} logs")
if errors:
parts.append(f"{len(errors)} errors")
if monitors:
parts.append(f"{len(monitors)} monitors")
if events:
parts.append(f"{len(events)} events")
fetch_ms = e.get("datadog_fetch_ms", {})
if fetch_ms:
max_ms = max(v for v in fetch_ms.values() if isinstance(v, int | float))
if max_ms > 0:
parts.append(f"fetched in {max_ms / 1000:.1f}s")
return ", ".join(parts)
# (label, count_fn, url_fn) — url_fn receives evidence and returns str|None
ACTION_DEFS: dict[str, tuple[str, Any, Any]] = {
"get_cloudwatch_logs": (
"cloudwatch logs",
lambda e: (
f"{len(e.get('cloudwatch_logs', []))} events" if e.get("cloudwatch_logs") else None
),
None,
),
"get_error_logs": (
"error logs",
lambda e: f"{len(e.get('error_logs', []))} logs" if e.get("error_logs") else None,
None,
),
"get_failed_jobs": (
"batch jobs",
lambda e: f"{len(e.get('failed_jobs', []))} failed" if e.get("failed_jobs") else None,
None,
),
"get_failed_tools": (
"failed tools",
lambda e: f"{len(e.get('failed_tools', []))} failed" if e.get("failed_tools") else None,
None,
),
"get_lambda_invocation_logs": (
"lambda logs",
lambda e: f"{len(e.get('lambda_logs', []))} logs" if e.get("lambda_logs") else None,
None,
),
"get_lambda_errors": (
"lambda errors",
lambda e: (
f"{len(e.get('lambda_errors', []))} errors" if e.get("lambda_errors") else None
),
None,
),
"inspect_s3_object": (
"S3 object",
lambda e: "found" if (e.get("s3_object") or {}).get("found") else None,
None,
),
"get_s3_object": (
"S3 audit payload",
lambda e: "retrieved" if (e.get("s3_audit_payload") or {}).get("found") else None,
None,
),
"inspect_lambda_function": (
"lambda function",
lambda e: "inspected" if e.get("lambda_function") else None,
None,
),
"query_grafana_logs": (
"Grafana Loki",
_grafana_logs_count,
lambda e: (
build_grafana_explore_url(
grafana_endpoint,
e.get("grafana_logs_query", ""),
)
if grafana_endpoint and e.get("grafana_logs_query")
else None
),
),
"query_grafana_traces": (
"Grafana Tempo",
lambda e: (
f"{len(e.get('grafana_traces', []))} traces" if e.get("grafana_traces") else None
),
lambda _: f"{grafana_endpoint.rstrip('/')}/explore" if grafana_endpoint else None,
),
"query_grafana_metrics": (
"Grafana Mimir",
lambda e: (
f"{len(e.get('grafana_metrics', []))} metrics" if e.get("grafana_metrics") else None
),
lambda _: f"{grafana_endpoint.rstrip('/')}/explore" if grafana_endpoint else None,
),
"query_grafana_alert_rules": (
"Grafana alerts",
lambda e: (
f"{len(e.get('grafana_alert_rules', []))} rules"
if e.get("grafana_alert_rules")
else None
),
lambda _: f"{grafana_endpoint.rstrip('/')}/alerting/list" if grafana_endpoint else None,
),
"query_datadog_all": (
"Datadog",
_datadog_investigate_count,
lambda e: (
build_datadog_logs_url(
e.get("datadog_logs_query", ""),
datadog_site,
)
if e.get("datadog_logs_query")
else f"https://app.{datadog_site}/logs"
),
),
"query_datadog_logs": (
"Datadog Logs",
_datadog_logs_count,
lambda e: (
build_datadog_logs_url(
e.get("datadog_logs_query", ""),
datadog_site,
)
if e.get("datadog_logs_query")
else f"https://app.{datadog_site}/logs"
),
),
"query_datadog_monitors": (
"Datadog Monitors",
lambda e: (
f"{len(e.get('datadog_monitors', []))} monitors"
if e.get("datadog_monitors")
else None
),
lambda _: f"https://app.{datadog_site}/monitors/manage",
),
"query_datadog_events": (
"Datadog Events",
lambda e: (
f"{len(e.get('datadog_events', []))} events" if e.get("datadog_events") else None
),
lambda _: f"https://app.{datadog_site}/event/explorer",
),
"query_betterstack_logs": (
"Better Stack Logs",
lambda e: (
f"{len(e.get('betterstack_logs', []))} rows" if e.get("betterstack_logs") else None
),
None, # Better Stack SQL endpoint has no user-facing deep-link URL
),
}
parts: list[str] = []
for action in all_actions:
defn = ACTION_DEFS.get(action)
if defn:
label, count_fn, url_fn = defn
count_str = count_fn(evidence)
url = url_fn(evidence) if url_fn else None
display = link_fn(label, url or None)
if count_str:
parts.append(f"{display} ({count_str})")
else:
parts.append(display)
else:
parts.append(action.replace("_", " "))
return "Queries: " + ", ".join(parts)
def format_cited_evidence_section(ctx: ReportContext) -> str:
"""Format the cited evidence section of the report.
Shows catalog entries as linked E-id citations, plus a compact summary
of tool calls made during investigation.
Returns empty string if there is nothing to show.
"""
catalog = ctx.get("evidence_catalog") or {}
lines: list[str] = []
if catalog:
def _sort_key(eid: str) -> str:
return str(catalog[eid].get("display_id", eid))
for evidence_id in sorted(catalog.keys(), key=_sort_key):
# Per-pod entries are shown in the Failed Pods section — skip them here
if evidence_id.startswith("evidence/datadog/failed_pod/"):
continue
entry = catalog[evidence_id] or {}
display_id = entry.get("display_id", evidence_id)
label = entry.get("label") or evidence_id
url = entry.get("url")
summary = entry.get("summary")
snippet = entry.get("snippet")
provenance = entry.get("provenance")
link = format_slack_link(label, url or None)
line = f"- {display_id}{link}"
if summary:
line += f" — {summary}"
if provenance:
line += f" — provenance: {provenance}"
if snippet:
line += f" — {shorten_text(snippet, max_chars=100)}"
lines.append(line)
tool_calls_line = _format_tool_calls_line(ctx)
if tool_calls_line:
lines.append(f"- {tool_calls_line}")
if not lines:
return ""
return "\n*Cited Evidence:*\n" + "\n".join(lines) + "\n"
def format_cited_evidence_section_html(ctx: ReportContext) -> str:
"""Like :func:`format_cited_evidence_section` but Telegram HTML with consistent bullets."""
catalog = ctx.get("evidence_catalog") or {}
lines: list[str] = []
if catalog:
def _sort_key(eid: str) -> str:
return str(catalog[eid].get("display_id", eid))
for evidence_id in sorted(catalog.keys(), key=_sort_key):
if evidence_id.startswith("evidence/datadog/failed_pod/"):
continue
entry = catalog[evidence_id] or {}
display_id = entry.get("display_id", evidence_id)
label = entry.get("label") or evidence_id
url = entry.get("url")
summary = entry.get("summary")
snippet = entry.get("snippet")
provenance = entry.get("provenance")
link = format_html_link(label, url or None)
line = f"• {html.escape(str(display_id))}{link}"
if summary:
line += f" — {html.escape(str(summary))}"
if provenance:
line += f" — provenance: {html.escape(str(provenance))}"
if snippet:
line += f" — {html.escape(shorten_text(snippet, max_chars=100))}"
lines.append(line)
tool_calls_line = _format_tool_calls_line(ctx, link_fn=format_html_link)
if tool_calls_line:
lines.append(f"• {tool_calls_line}")
if not lines:
return ""
return "\n<b>Cited Evidence</b>\n" + "\n".join(lines) + "\n"