fix(agent): persist signal incidents with tenant context
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
This commit is contained in:
@@ -1120,11 +1120,12 @@ class TimelineDBService:
|
||||
risk_level: str | None = None,
|
||||
approval_id: str | None = None,
|
||||
incident_id: str | None = None,
|
||||
project_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
新增時間軸事件
|
||||
"""
|
||||
async with get_db_context() as db:
|
||||
async with get_db_context(project_id) as db:
|
||||
event = TimelineEvent(
|
||||
event_type=event_type,
|
||||
status=status,
|
||||
|
||||
@@ -97,6 +97,8 @@ def _derive_incident_alert_metadata(incident: Incident) -> dict[str, Any]:
|
||||
async def _add_signal_timeline_event(
|
||||
incident: Incident,
|
||||
metadata: dict[str, Any],
|
||||
*,
|
||||
project_id: str,
|
||||
) -> None:
|
||||
"""Best-effort timeline seed for incidents created outside Alertmanager."""
|
||||
alertname = metadata.get("alertname")
|
||||
@@ -115,6 +117,7 @@ async def _add_signal_timeline_event(
|
||||
actor_role="signal_worker",
|
||||
risk_level=getattr(incident.severity, "value", incident.severity),
|
||||
incident_id=incident.incident_id,
|
||||
project_id=project_id,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
@@ -137,10 +140,13 @@ class IncidentDbAdapter:
|
||||
注入到 lewooogo-brain 的 DualIncidentMemory
|
||||
"""
|
||||
|
||||
def __init__(self, project_id: str = "awoooi") -> None:
|
||||
self._project_id = project_id
|
||||
|
||||
async def load(self, incident_id: str) -> Incident | None:
|
||||
"""從 PostgreSQL 載入 Incident"""
|
||||
try:
|
||||
async with get_db_context() as db:
|
||||
async with get_db_context(self._project_id) as db:
|
||||
from sqlalchemy import select
|
||||
stmt = select(IncidentRecord).where(
|
||||
IncidentRecord.incident_id == incident_id
|
||||
@@ -161,7 +167,7 @@ class IncidentDbAdapter:
|
||||
metadata = _derive_incident_alert_metadata(incident)
|
||||
created = False
|
||||
try:
|
||||
async with get_db_context() as db:
|
||||
async with get_db_context(self._project_id) as db:
|
||||
from sqlalchemy import select
|
||||
|
||||
stmt = select(IncidentRecord).where(
|
||||
@@ -191,6 +197,7 @@ class IncidentDbAdapter:
|
||||
else:
|
||||
record = IncidentRecord(
|
||||
incident_id=incident.incident_id,
|
||||
project_id=self._project_id,
|
||||
status=incident.status.value,
|
||||
severity=incident.severity.value,
|
||||
signals=[
|
||||
@@ -222,7 +229,11 @@ class IncidentDbAdapter:
|
||||
created = True
|
||||
|
||||
if created:
|
||||
await _add_signal_timeline_event(incident, metadata)
|
||||
await _add_signal_timeline_event(
|
||||
incident,
|
||||
metadata,
|
||||
project_id=self._project_id,
|
||||
)
|
||||
logger.debug("db_adapter_save_success", incident_id=incident.incident_id)
|
||||
return True
|
||||
|
||||
|
||||
@@ -436,6 +436,7 @@ async def _add_observation_timeline(
|
||||
actor_role="signal_worker",
|
||||
risk_level=_severity_value(getattr(incident, "severity", None)),
|
||||
incident_id=_incident_id(incident),
|
||||
project_id=PROJECT_ID,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
@@ -477,7 +478,10 @@ async def _ensure_signal_observation_mcp_runtime() -> None:
|
||||
try:
|
||||
from src.plugins.mcp.providers import register_all_providers
|
||||
from src.plugins.mcp.registry import get_provider_registry
|
||||
from src.services.mcp_tool_registry import get_mcp_tool_registry, init_mcp_tool_registry
|
||||
from src.services.mcp_tool_registry import (
|
||||
get_mcp_tool_registry,
|
||||
init_mcp_tool_registry,
|
||||
)
|
||||
|
||||
provider_registry = get_provider_registry()
|
||||
if len(provider_registry) == 0:
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from src.models.incident import Severity
|
||||
from src.services.incident_memory import _derive_incident_alert_metadata
|
||||
from src.services import incident_memory as incident_memory_module
|
||||
from src.services.incident_memory import (
|
||||
IncidentDbAdapter,
|
||||
_derive_incident_alert_metadata,
|
||||
)
|
||||
|
||||
|
||||
def test_signal_worker_incident_metadata_uses_signal_alert_name() -> None:
|
||||
@@ -30,3 +37,31 @@ def test_signal_worker_incident_metadata_uses_signal_alert_name() -> None:
|
||||
assert metadata["description"] == "29 ERROR log entries in last 5min"
|
||||
assert metadata["actor"] == "journal"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_incident_db_adapter_uses_explicit_project_context(monkeypatch) -> None:
|
||||
project_ids: list[str | None] = []
|
||||
|
||||
class FakeResult:
|
||||
def scalar_one_or_none(self):
|
||||
return None
|
||||
|
||||
class FakeDb:
|
||||
async def execute(self, _statement):
|
||||
return FakeResult()
|
||||
|
||||
@asynccontextmanager
|
||||
async def fake_get_db_context(project_id=None):
|
||||
project_ids.append(project_id)
|
||||
yield FakeDb()
|
||||
|
||||
monkeypatch.setattr(
|
||||
incident_memory_module,
|
||||
"get_db_context",
|
||||
fake_get_db_context,
|
||||
)
|
||||
|
||||
result = await IncidentDbAdapter(project_id="awoooi").load("INC-NOT-FOUND")
|
||||
|
||||
assert result is None
|
||||
assert project_ids == ["awoooi"]
|
||||
|
||||
@@ -2,7 +2,10 @@ from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from src.models.incident import Severity
|
||||
from src.services import signal_observation_service as signal_observation_module
|
||||
from src.services.signal_observation_service import (
|
||||
build_signal_worker_provider_event_id,
|
||||
enrich_incident_for_signal_observation,
|
||||
@@ -72,3 +75,26 @@ def test_signal_worker_event_content_is_operator_readable() -> None:
|
||||
assert "Incident: INC-20260518-TEST01" in content
|
||||
assert "Alert: HostErrorLogFlood" in content
|
||||
assert "Telegram: not sent" in content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_signal_observation_timeline_uses_explicit_project_context(monkeypatch) -> None:
|
||||
calls: list[dict] = []
|
||||
|
||||
class FakeTimelineService:
|
||||
async def add_event(self, **kwargs):
|
||||
calls.append(kwargs)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"src.services.approval_db.get_timeline_service",
|
||||
lambda: FakeTimelineService(),
|
||||
)
|
||||
|
||||
await signal_observation_module._add_observation_timeline(
|
||||
incident=_host_incident(),
|
||||
signal_data={"alert_name": "HostErrorLogFlood"},
|
||||
labels={"target": "ollama"},
|
||||
raw_evidence_created=True,
|
||||
)
|
||||
|
||||
assert calls[0]["project_id"] == "awoooi"
|
||||
|
||||
Reference in New Issue
Block a user