fix(awooop): link ansible evidence to incidents
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
""",
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user