f8616b806e
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---
98 lines
3.5 KiB
Python
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)) |