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
155 lines
4.9 KiB
Python
155 lines
4.9 KiB
Python
"""Deterministic alert normalization helpers for extract_alert."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from collections.abc import Mapping
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
from core.domain.alerts.fields import (
|
|
alert_annotations,
|
|
alert_labels,
|
|
alert_name_value,
|
|
canonical_alert,
|
|
pipeline_name_value,
|
|
severity_value,
|
|
)
|
|
|
|
CANONICAL_ALERT_SOURCES = frozenset({"opensre", "opensre_dataset"})
|
|
|
|
RAW_ALERT_DETAIL_FIELDS = (
|
|
"kube_namespace",
|
|
"cloudwatch_log_group",
|
|
"error_message",
|
|
"log_query",
|
|
"eks_cluster",
|
|
"pod_name",
|
|
"deployment",
|
|
)
|
|
|
|
|
|
class AlertDetails(BaseModel):
|
|
"""Normalized alert fields produced by the extract_alert stage."""
|
|
|
|
is_noise: bool = Field(default=False)
|
|
alert_name: str = Field(default="unknown")
|
|
pipeline_name: str = Field(default="unknown")
|
|
severity: str = Field(default="unknown")
|
|
alert_source: str | None = Field(default=None)
|
|
environment: str | None = Field(default=None)
|
|
summary: str | None = Field(default=None)
|
|
kube_namespace: str | None = Field(default=None)
|
|
cloudwatch_log_group: str | None = Field(default=None)
|
|
error_message: str | None = Field(default=None)
|
|
log_query: str | None = Field(default=None)
|
|
eks_cluster: str | None = Field(default=None)
|
|
pod_name: str | None = Field(default=None)
|
|
deployment: str | None = Field(default=None)
|
|
|
|
|
|
def format_raw_alert(raw_alert: Any) -> str:
|
|
"""Render raw alert payload as prompt text for the extract_alert LLM call."""
|
|
if isinstance(raw_alert, str):
|
|
return raw_alert
|
|
if isinstance(raw_alert, dict):
|
|
if raw_alert.get("text") and not needs_full_json_prompt(raw_alert):
|
|
return str(raw_alert["text"])
|
|
return json.dumps(raw_alert, indent=2, sort_keys=True)
|
|
return json.dumps(raw_alert, indent=2, sort_keys=True)
|
|
|
|
|
|
def needs_full_json_prompt(raw_alert: dict[str, Any]) -> bool:
|
|
"""Return True when extract_alert should receive full JSON instead of text-only."""
|
|
src = str(raw_alert.get("alert_source", "")).lower()
|
|
if src in CANONICAL_ALERT_SOURCES:
|
|
return True
|
|
if (
|
|
raw_alert.get("commonLabels")
|
|
or raw_alert.get("commonAnnotations")
|
|
or raw_alert.get("alerts")
|
|
):
|
|
return True
|
|
for key in (
|
|
"opensre_telemetry_relative",
|
|
"opensre_dataset_root",
|
|
):
|
|
if raw_alert.get(key):
|
|
return True
|
|
ann = raw_alert.get("commonAnnotations")
|
|
if isinstance(ann, dict) and ann.get(key):
|
|
return True
|
|
meta = raw_alert.get("_meta")
|
|
return bool(isinstance(meta, dict) and "opensre" in str(meta.get("purpose", "")).lower())
|
|
|
|
|
|
def fallback_details(state: Mapping[str, Any], raw_alert: Any) -> AlertDetails:
|
|
"""Best-effort field extraction when the LLM path is unavailable."""
|
|
alert_name = state.get("alert_name", "unknown")
|
|
pipeline_name = state.get("pipeline_name", "unknown")
|
|
severity = state.get("severity", "unknown")
|
|
|
|
if isinstance(raw_alert, dict):
|
|
labels = alert_labels(raw_alert)
|
|
annotations = alert_annotations(raw_alert)
|
|
canonical = canonical_alert(raw_alert)
|
|
|
|
alert_name = alert_name_value(
|
|
raw_alert,
|
|
labels=labels,
|
|
annotations=annotations,
|
|
canonical=canonical,
|
|
fallback=alert_name,
|
|
)
|
|
pipeline_name = pipeline_name_value(
|
|
raw_alert,
|
|
labels=labels,
|
|
annotations=annotations,
|
|
canonical=canonical,
|
|
fallback=pipeline_name,
|
|
)
|
|
severity = severity_value(
|
|
raw_alert,
|
|
labels=labels,
|
|
canonical=canonical,
|
|
fallback=severity,
|
|
)
|
|
|
|
return AlertDetails(
|
|
is_noise=False,
|
|
alert_name=alert_name or "unknown",
|
|
pipeline_name=pipeline_name or "unknown",
|
|
severity=severity or "unknown",
|
|
)
|
|
|
|
|
|
def make_problem_md(details: AlertDetails) -> str:
|
|
"""Build the operator-facing problem markdown header from extracted details."""
|
|
parts = [
|
|
f"# {details.alert_name}",
|
|
f"Pipeline: {details.pipeline_name} | Severity: {details.severity}",
|
|
]
|
|
if details.kube_namespace:
|
|
parts.append(f"Namespace: {details.kube_namespace}")
|
|
if details.error_message:
|
|
parts.append(f"\nError: {details.error_message}")
|
|
return "\n".join(parts)
|
|
|
|
|
|
def enrich_raw_alert(raw_alert: Any, details: AlertDetails) -> Any:
|
|
"""Merge extracted details back into the raw alert dict for downstream stages."""
|
|
if not isinstance(raw_alert, dict):
|
|
raw_alert = {}
|
|
enriched = dict(raw_alert)
|
|
prior_source = str(raw_alert.get("alert_source", "")).lower()
|
|
|
|
for field_name in RAW_ALERT_DETAIL_FIELDS:
|
|
value = getattr(details, field_name)
|
|
if value:
|
|
enriched[field_name] = value
|
|
|
|
if details.alert_source and prior_source not in CANONICAL_ALERT_SOURCES:
|
|
enriched["alert_source"] = details.alert_source
|
|
return enriched
|