Files
tracer-cloud--opensre/core/domain/alerts/alert_source.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

256 lines
9.8 KiB
Python

"""Alert source resolution and tool-source routing helpers.
Naming convention:
- ``alert_source`` — vendor/format key on the incoming alert payload (e.g.
``"grafana"``, ``"eks"``). Keys ``ALERT_SOURCE_ROUTING``.
- ``tool source`` — integration key matching ``tool.source`` (e.g.
``"grafana"``, ``"ec2"``, ``"cloudtrail"``). Values in routing tuples.
Each ``AlertSourceRouting`` entry carries two tool-source lists:
- ``relevance_tool_sources`` — broad prioritization during tool planning.
- ``seed_tool_sources`` — narrower subset auto-invoked before the LLM loop.
Expensive or context-dependent tools stay out of seeding.
"""
from __future__ import annotations
from collections.abc import Iterable
from dataclasses import dataclass
from typing import Any
from core.domain.alerts.fields import iter_alert_blocks
@dataclass(frozen=True)
class AlertSourceRouting:
relevance_tool_sources: tuple[str, ...]
seed_tool_sources: tuple[str, ...]
def _routing(
relevance_tool_sources: tuple[str, ...],
seed_tool_sources: tuple[str, ...],
) -> AlertSourceRouting:
return AlertSourceRouting(
relevance_tool_sources=relevance_tool_sources,
seed_tool_sources=seed_tool_sources,
)
# Single registry — relevance and seed lists are intentionally different per entry.
ALERT_SOURCE_ROUTING: dict[str, AlertSourceRouting] = {
"grafana": _routing(("grafana",), ("grafana",)),
"datadog": _routing(("datadog",), ("datadog",)),
# ec2/rds/cloudtrail stay relevance-only — context-dependent pre-LLM seeding.
"cloudwatch": _routing(("cloudwatch", "ec2", "rds", "cloudtrail"), ("cloudwatch",)),
# ec2/cloudtrail stay relevance-only — seed only the cluster integration.
"eks": _routing(("eks", "ec2", "cloudtrail"), ("eks",)),
# eks/cloudtrail stay relevance-only — seed grafana + cloudwatch dashboards/logs.
"alertmanager": _routing(
("eks", "cloudwatch", "grafana", "cloudtrail"),
("grafana", "cloudwatch"),
),
"sentry": _routing(("sentry",), ("sentry",)),
"honeycomb": _routing(("honeycomb",), ("honeycomb",)),
"coralogix": _routing(("coralogix",), ("coralogix",)),
# tracer_web stays relevance-only — secondary web context, not pre-LLM seed.
"airflow": _routing(("airflow", "tracer_web"), ("airflow",)),
"hermes": _routing(("hermes",), ("hermes",)),
"kafka": _routing(("kafka",), ("kafka",)),
"postgresql": _routing(("postgresql",), ("postgresql",)),
"mysql": _routing(("mysql",), ("mysql",)),
"mariadb": _routing(("mariadb",), ("mariadb",)),
"mongodb": _routing(("mongodb", "mongodb_atlas"), ("mongodb", "mongodb_atlas")),
"redis": _routing(("redis",), ("redis",)),
"snowflake": _routing(("snowflake",), ("snowflake",)),
"clickhouse": _routing(("clickhouse",), ("clickhouse",)),
"dagster": _routing(("dagster",), ("dagster",)),
"rabbitmq": _routing(("rabbitmq",), ("rabbitmq",)),
"supabase": _routing(("supabase",), ("supabase",)),
"opensearch": _routing(("opensearch",), ("opensearch",)),
"openobserve": _routing(("openobserve",), ("openobserve",)),
"betterstack": _routing(("betterstack",), ("betterstack",)),
"azure": _routing(("azure", "azure_sql"), ("azure", "azure_sql")),
"github": _routing(("github",), ("github",)),
"gitlab": _routing(("gitlab",), ("gitlab",)),
"bitbucket": _routing(("bitbucket",), ("bitbucket",)),
"argocd": _routing(("eks",), ("eks",)),
"splunk": _routing(("splunk",), ("splunk",)),
"signoz": _routing(("signoz",), ("signoz",)),
"jenkins": _routing(("jenkins",), ("jenkins",)),
"tempo": _routing(("tempo",), ("tempo",)),
"temporal": _routing(("temporal",), ("temporal",)),
}
# Generic fallback sources: useful, but never primary when incident-specific
# integrations match.
SECONDARY_TOOL_SOURCES = frozenset({"knowledge", "openclaw", "google_docs"})
DB_KEYWORDS: tuple[str, ...] = ("database", "db connection", "connection pool")
SOURCE_ALIASES: dict[str, tuple[str, ...]] = {
"datadog": ("datadog", "datadoghq", "dd monitor"),
"sentry": ("sentry", "exception", "stack trace", "stacktrace", "error tracking"),
"vercel": ("vercel", "deploy", "deployment", "build failed"),
"github": ("github", "commit", "pull request", "merge"),
"gitlab": ("gitlab", "merge request"),
"grafana": ("grafana", "loki", "mimir", "prometheus"),
"honeycomb": ("honeycomb", "span", "trace latency"),
"coralogix": ("coralogix",),
"splunk": ("splunk",),
"cloudwatch": ("cloudwatch", "lambda", "log group"),
"eks": ("eks", "kubernetes", "k8s", "kubectl", "pod"),
"ec2": ("ec2", "instance"),
"rds": ("rds", "aurora", *DB_KEYWORDS),
"postgresql": ("postgres", "postgresql", "psql", *DB_KEYWORDS),
"mysql": ("mysql", *DB_KEYWORDS),
"mariadb": ("mariadb", *DB_KEYWORDS),
"mongodb": ("mongodb", "mongo", *DB_KEYWORDS),
"redis": ("redis", "cache"),
"snowflake": ("snowflake",),
"clickhouse": ("clickhouse",),
"dagster": ("dagster",),
"airflow": ("airflow", "dag"),
"kafka": ("kafka",),
"rabbitmq": ("rabbitmq", "amqp"),
"supabase": ("supabase",),
"opensearch": ("opensearch", "elasticsearch"),
"openobserve": ("openobserve",),
"betterstack": ("betterstack", "better stack"),
"azure": ("azure",),
"signoz": ("signoz",),
"jenkins": ("jenkins",),
"tempo": ("tempo",),
"temporal": ("temporal", "temporal workflow", "task queue"),
}
def routing_for_alert_source(alert_source: str) -> AlertSourceRouting | None:
"""Return routing config for a resolved alert vendor key, if known."""
return ALERT_SOURCE_ROUTING.get(alert_source.strip().lower())
def primary_sources_for_alert(state: dict[str, Any]) -> tuple[str, ...]:
"""Return the routing entry's ``relevance_tool_sources`` for this alert.
Used for broad alert-driven tool prioritization; callers surface these
as ``primary_sources`` in plan audits and prompts.
"""
routing = routing_for_alert_source(resolve_alert_source(state))
return routing.relevance_tool_sources if routing is not None else ()
def seed_tool_sources_for_alert(state: dict[str, Any]) -> tuple[str, ...]:
"""Return tool sources auto-called before the investigation LLM loop."""
routing = routing_for_alert_source(resolve_alert_source(state))
return routing.seed_tool_sources if routing is not None else ()
def declared_context_sources(state: dict[str, Any]) -> set[str]:
"""Return explicit context source annotations from the raw alert, if any."""
raw = state.get("raw_alert")
if not isinstance(raw, dict):
return set()
for block in iter_alert_blocks(raw):
value = block.get("context_sources")
if isinstance(value, str) and value.strip():
return {item.strip().lower() for item in value.split(",") if item.strip()}
return set()
def collect_alert_text(state: dict[str, Any]) -> str:
"""Collect searchable alert text for deterministic source/tool matching."""
parts: list[str] = [
str(state.get("alert_name") or ""),
str(state.get("pipeline_name") or ""),
str(state.get("message") or ""),
]
raw = state.get("raw_alert")
if isinstance(raw, dict):
for key in ("alert_name", "title", "message", "text", "error_message", "kube_namespace"):
value = raw.get(key)
if isinstance(value, str):
parts.append(value)
for block in iter_alert_blocks(raw):
parts.extend(str(v) for v in block.values() if isinstance(v, (str, int, float)))
elif isinstance(raw, str):
parts.append(raw)
problem_md = state.get("problem_md")
if isinstance(problem_md, str):
parts.append(problem_md)
return " ".join(part for part in parts if part).lower()
def relevant_sources_for_alert(
state: dict[str, Any],
candidate_sources: Iterable[str],
) -> list[str]:
"""Select candidate sources relevant to the alert content."""
candidates = sorted(
source for source in candidate_sources if source not in SECONDARY_TOOL_SOURCES
)
if not candidates:
return []
declared = declared_context_sources(state)
if declared:
from_declared = [source for source in candidates if source in declared]
if from_declared:
return from_declared
text = collect_alert_text(state)
if not text:
return []
matched: list[str] = []
for source in candidates:
keywords = {source, *SOURCE_ALIASES.get(source, ())}
if any(keyword in text for keyword in keywords):
matched.append(source)
return matched
def resolve_alert_source(state: dict[str, Any]) -> str:
"""Return the alert vendor key used to look up tool-source routing.
Grafana managed alerts reuse the Alertmanager webhook schema, so
``alert_source`` is often missing from the payload — we sniff
``grafana_folder`` / ``datasource_uid`` labels and ``externalURL`` below.
"""
source = str(state.get("alert_source") or "").lower().strip()
if source:
return source
raw = state.get("raw_alert")
if isinstance(raw, dict):
source = str(raw.get("alert_source") or "").lower().strip()
if source:
return source
labels = raw.get("commonLabels") or raw.get("labels") or {}
if isinstance(labels, dict) and (
labels.get("grafana_folder") or labels.get("datasource_uid")
):
return "grafana"
ext_url = raw.get("externalURL", "")
if isinstance(ext_url, str) and "grafana" in ext_url.lower():
return "grafana"
return ""
__all__ = [
"ALERT_SOURCE_ROUTING",
"AlertSourceRouting",
"SECONDARY_TOOL_SOURCES",
"SOURCE_ALIASES",
"collect_alert_text",
"declared_context_sources",
"primary_sources_for_alert",
"relevant_sources_for_alert",
"resolve_alert_source",
"routing_for_alert_source",
"seed_tool_sources_for_alert",
]