Files
acdl/core/metrics/event_envelope.py
T
Jon Chery f8616b806e feat(P1): event emitters — CloudEvents envelope, Decision Ledger, Infracost adapter, attestation/confidence/policy event emission
P1 (Wave 1, feat) — REQ-187, REQ-188, REQ-205 (emitter), REQ-206 (emitter)

New components:
- core/metrics/event_envelope.py — CloudEvents 1.0 envelope + platform.* conventions
- core/metrics/run_manifest.py — per-run manifest writer (nova.run.started/completed/failed)
- core/metrics/decision_ledger.py — SQLite append-only hash-chain (ai.decision.made + attestation.recorded)
- core/metrics/infracost_adapter.py — Infracost post-processor (degraded mode when CLI absent, A6)
- schemas/metrics_event.schema.json — CloudEvents envelope schema
- schemas/metrics_run_manifest.schema.json — per-run manifest schema
- metrics/README.md — backup/restore doc (REQ-201)
- tests/test_metrics_emitters.py — 16 tests (all pass)

Modified components:
- core/confidence_signal.py — emits nova.confidence.computed + nova.ai.decision.made (D-122)
- core/hitl_gates.py — emits nova.attestation.recorded on qa/prod/dr gates (D-132)
- adapters/terraform/policy/checkov_adapter.py — emits nova.policy.evaluated
- pyproject.toml — addopts gains --junitxml + --json-report + --cov (REQ-206)
- .gitignore — metrics runtime artifacts ignored

D-120: Nova-native (JSONL + SQLite, no Kafka/OTel)
D-121: Decision Ledger = outbox_writer extension → SQLite hash-chain
D-122: AI decision = confidence_signal + HITL gate (not LLM)
D-128: metrics/ at repo root
D-132: Attestation instrumentation

---ci---
project: acdl
phase: 1
milestone: v1.17
status: execute
---/ci---
2026-08-04 19:58:54 +00:00

98 lines
3.5 KiB
Python

"""Nova CloudEvents 1.0 envelope + platform.* semantic conventions (REQ-187).
Defines the standard event envelope for all Nova metrics events. Every
emitter (run_manifest, decision_ledger, confidence_signal, checkov_adapter,
hitl_gates, regression_verify) uses `make_event()` to produce a valid
CloudEvents 1.0 envelope. Events are appended to `metrics/events.jsonl`.
D-120: Nova-native minimal tech (no Kafka/OTel SDK — JSONL + SQLite).
D-125: hybrid model — existing file signals stay as files; the collector
reads them and emits normalized CloudEvents. New emitters emit directly.
"""
import datetime
import hashlib
import json
import os
import sys
import uuid
METRICS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "metrics")
EVENTS_LOG = os.path.join(METRICS_DIR, "events.jsonl")
def _iso8601_now():
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def make_event(event_type, run_id, environment, data, contract_id="", source="nova.platform", subject="", actor_type="confidence-gate", actor_id="confidence_signal"):
"""Build a CloudEvents 1.0 envelope with Nova platform.* conventions.
Args:
event_type: e.g. "nova.run.completed", "nova.ai.decision.made"
run_id: the run identifier (e.g. "run-<epoch>")
environment: dev|qa|prod|dr
data: the event payload dict
contract_id: the contract UUID (optional)
source: the event source (default "nova.platform")
subject: the event subject (default "<contract_id>/<env>")
actor_type: the actor type (default "confidence-gate")
actor_id: the actor id (default "confidence_signal")
Returns:
A CloudEvents 1.0 envelope dict.
"""
if not subject:
subject = f"{contract_id}/{environment}" if contract_id else environment
return {
"specversion": "1.0",
"id": str(uuid.uuid4()),
"source": source,
"type": event_type,
"time": _iso8601_now(),
"subject": subject,
"datacontenttype": "application/json",
"platform": {
"tenant_id": "acdl",
"run_id": run_id,
"contract_id": contract_id,
"environment": environment,
"actor": {"type": actor_type, "id": actor_id},
"trace_id": run_id,
},
"data": data,
}
def append_event(event, events_log=None):
"""Append a CloudEvents envelope to the JSONL event log.
Creates the metrics/ directory if it doesn't exist.
"""
if events_log is None:
events_log = EVENTS_LOG
os.makedirs(os.path.dirname(events_log), exist_ok=True)
with open(events_log, "a", encoding="utf-8") as fh:
fh.write(json.dumps(event, sort_keys=True, separators=(",", ":")) + "\n")
def emit(event_type, run_id, environment, data, **kwargs):
"""Make an event + append it to the JSONL log. Convenience wrapper."""
event = make_event(event_type, run_id, environment, data, **kwargs)
append_event(event)
return event
if __name__ == "__main__":
if len(sys.argv) < 4:
print("usage: event_envelope.py <event_type> <run_id> <environment> [data.json]", file=sys.stderr)
sys.exit(2)
_type = sys.argv[1]
_run_id = sys.argv[2]
_env = sys.argv[3]
_data = {}
if len(sys.argv) >= 5 and os.path.isfile(sys.argv[4]):
with open(sys.argv[4]) as f:
_data = json.load(f)
ev = emit(_type, _run_id, _env, _data)
print(json.dumps(ev, indent=2))