fix(awooop): link ansible evidence to incidents
All checks were successful
CD Pipeline / tests (push) Successful in 1m21s
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / build-and-deploy (push) Successful in 4m42s
CD Pipeline / post-deploy-checks (push) Successful in 1m53s

This commit is contained in:
Your Name
2026-05-31 13:36:51 +08:00
parent 28cd4b01fe
commit dad8c0fbfc
4 changed files with 87 additions and 21 deletions

View File

@@ -266,6 +266,7 @@ def build_ansible_truth(
"schema_version": "ansible_executor_audit_v1",
"operation_types": sorted(ANSIBLE_OPERATION_TYPES),
"required_audit_fields": [
"incident_id",
"operation_type",
"status",
"actor",
@@ -404,7 +405,7 @@ async def record_ansible_decision_audit(
SELECT op_id
FROM automation_operation_log
WHERE operation_type = 'ansible_candidate_matched'
AND input ->> 'incident_id' = :incident_id
AND coalesce(incident_id::text, input ->> 'incident_id') = :incident_id
AND input ->> 'executor' = 'ansible'
LIMIT 1
"""),
@@ -415,12 +416,13 @@ async def record_ansible_decision_audit(
await db.execute(
text("""
INSERT INTO automation_operation_log (
operation_type, actor, status,
operation_type, actor, status, incident_id,
input, output, dry_run_result, tags
) VALUES (
:operation_type,
'decision_manager',
:status,
NULLIF(:incident_id, ''),
CAST(:input AS jsonb),
CAST(:output AS jsonb),
CAST(:dry_run_result AS jsonb),
@@ -430,6 +432,7 @@ async def record_ansible_decision_audit(
{
"operation_type": payload["operation_type"],
"status": payload["status"],
"incident_id": incident_id,
"input": json.dumps(payload["input"], ensure_ascii=False),
"output": json.dumps(payload["output"], ensure_ascii=False),
"dry_run_result": json.dumps(payload["dry_run_result"], ensure_ascii=False),

View File

@@ -81,6 +81,10 @@ def _json_loads(value: Any) -> dict[str, Any]:
return {}
def _incident_id_from_payload(payload: dict[str, Any]) -> str:
return str(payload.get("incident_id") or "").strip()
def detect_ansible_transport_blockers(*values: Any) -> list[str]:
combined = " ".join(str(value or "") for value in values)
blockers: list[str] = []
@@ -159,7 +163,7 @@ def build_ansible_check_mode_claim_input(
candidate_input: dict[str, Any],
) -> dict[str, Any]:
safe = _safe_candidate(candidate_input)
incident_id = str(candidate_input.get("incident_id") or "")
incident_id = _incident_id_from_payload(candidate_input)
return {
"incident_id": incident_id,
"executor": "ansible",
@@ -355,13 +359,14 @@ async def claim_pending_check_modes(
inserted = await db.execute(
text("""
INSERT INTO automation_operation_log (
operation_type, actor, status,
operation_type, actor, status, incident_id,
input, output, dry_run_result,
parent_op_id, tags
) VALUES (
'ansible_check_mode_executed',
'ansible_check_mode_worker',
'pending',
NULLIF(:incident_id, ''),
CAST(:input AS jsonb),
'{}'::jsonb,
CAST(:dry_run_result AS jsonb),
@@ -371,6 +376,7 @@ async def claim_pending_check_modes(
RETURNING op_id
"""),
{
"incident_id": _incident_id_from_payload(claim_input),
"input": json.dumps(claim_input, ensure_ascii=False),
"dry_run_result": json.dumps({
"check_mode_executed": False,
@@ -442,7 +448,7 @@ async def _insert_skipped_candidate(
reason: str,
) -> None:
input_payload = {
"incident_id": str(candidate_input.get("incident_id") or ""),
"incident_id": _incident_id_from_payload(candidate_input),
"executor": "ansible",
"execution_backend": "ansible",
"execution_mode": "check_mode",
@@ -454,13 +460,14 @@ async def _insert_skipped_candidate(
await db.execute(
text("""
INSERT INTO automation_operation_log (
operation_type, actor, status,
operation_type, actor, status, incident_id,
input, output, dry_run_result,
parent_op_id, tags
) VALUES (
'ansible_execution_skipped',
'ansible_check_mode_worker',
'dry_run',
NULLIF(:incident_id, ''),
CAST(:input AS jsonb),
CAST(:output AS jsonb),
CAST(:dry_run_result AS jsonb),
@@ -469,6 +476,7 @@ async def _insert_skipped_candidate(
)
"""),
{
"incident_id": _incident_id_from_payload(input_payload),
"input": json.dumps(input_payload, ensure_ascii=False),
"output": json.dumps({
"not_used_reason": reason,

View File

@@ -1652,22 +1652,55 @@ async def fetch_automation_quality_summary(
incidents = await _fetch_all(
db,
"""
WITH source_candidates AS (
SELECT
incident_id::text AS incident_id,
created_at AS seen_at
FROM incidents
WHERE (project_id = :project_id OR project_id IS NULL)
AND created_at >= :cutoff
UNION ALL
SELECT
coalesce(incident_id::text, input ->> 'incident_id') AS incident_id,
created_at AS seen_at
FROM automation_operation_log
WHERE created_at >= :cutoff
AND operation_type IN (
'ansible_candidate_matched',
'ansible_check_mode_executed',
'ansible_execution_skipped',
'ansible_apply_executed',
'ansible_rollback_executed'
)
AND coalesce(incident_id::text, input ->> 'incident_id', '') <> ''
),
source_ids AS (
SELECT
incident_id,
max(seen_at) AS recent_evidence_at
FROM source_candidates
WHERE incident_id IS NOT NULL AND incident_id <> ''
GROUP BY incident_id
)
SELECT
incident_id,
project_id,
status::text AS status,
severity::text AS severity,
alertname,
alert_category,
notification_type,
created_at,
updated_at,
resolved_at,
verification_result
FROM incidents
WHERE (project_id = :project_id OR project_id IS NULL)
AND created_at >= :cutoff
ORDER BY created_at DESC
incidents.incident_id,
incidents.project_id,
incidents.status::text AS status,
incidents.severity::text AS severity,
incidents.alertname,
incidents.alert_category,
incidents.notification_type,
incidents.created_at,
incidents.updated_at,
incidents.resolved_at,
incidents.verification_result,
source_ids.recent_evidence_at
FROM source_ids
JOIN incidents ON incidents.incident_id = source_ids.incident_id
WHERE (incidents.project_id = :project_id OR incidents.project_id IS NULL)
ORDER BY source_ids.recent_evidence_at DESC
LIMIT :limit
""",
{

View File

@@ -8,10 +8,12 @@ from types import SimpleNamespace
from src.services.awooop_ansible_audit_service import (
build_ansible_decision_audit_payload,
build_ansible_truth,
record_ansible_decision_audit,
)
from src.services.awooop_ansible_check_mode_service import (
build_ansible_check_mode_claim_input,
build_ansible_check_mode_command,
claim_pending_check_modes,
detect_ansible_transport_blockers,
)
from src.services.awooop_truth_chain_service import (
@@ -26,6 +28,7 @@ from src.services.awooop_truth_chain_service import (
build_automation_quality,
build_incident_reconciliation,
fetch_truth_chain,
fetch_automation_quality_summary,
summarize_automation_quality_records,
)
from src.services.drift_repeat_state import (
@@ -68,6 +71,25 @@ def test_fetch_truth_chain_can_match_inbound_provider_event_id() -> None:
assert "provider_event_id = :source_id" in source
def test_quality_summary_includes_recent_ansible_operation_incidents() -> None:
source = inspect.getsource(fetch_automation_quality_summary)
assert "FROM automation_operation_log" in source
assert "input ->> 'incident_id'" in source
assert "JOIN incidents ON incidents.incident_id = source_ids.incident_id" in source
assert "source_ids.recent_evidence_at DESC" in source
def test_ansible_audit_writes_incident_id_column_for_truth_chain_join() -> None:
decision_source = inspect.getsource(record_ansible_decision_audit)
claim_source = inspect.getsource(claim_pending_check_modes)
assert "operation_type, actor, status, incident_id" in decision_source
assert "coalesce(incident_id::text, input ->> 'incident_id')" in decision_source
assert "operation_type, actor, status, incident_id" in claim_source
assert "NULLIF(:incident_id, '')" in claim_source
def test_fetch_truth_chain_returns_inbound_redacted_envelope_fields() -> None:
source = inspect.getsource(fetch_truth_chain)