G-14 Audit correlation
Brick G of the Enterprise AI Platform. Technology: Developed component joining audit events, policy decisions, spans and usage events by trace_id, run_id and request_id. French version: ../../fr/bricks/G-14-audit-correlation.md.
Role and responsibilities
- Answer what happened in this run: decisions, tools, models, data, cost, in one timeline.
- Provide evidence packages for incidents and audits.
Position and interactions
- Execution path: read path, investigation.
- Used by the portal run view, incidents (brick A) and auditors.
Open source and developed parts
- Open source reused: None: SQL and ClickHouse joins.
- Developed in the platform: Whole component: correlation keys, timeline API, export with hashes.
Data model
Control Plane tables (schema eai): eai.audit_events, eai.telemetry_spans, eai.usage_events.
Every tenant table has tenant_id uuid NOT NULL and a row level security policy on current_setting('app.tenant_id', true)::uuid. ClickHouse usage_events is the analytics replica of eai.usage_events (ReplacingMergeTree keyed by event_id).
Contract: contracts/v1/audit-event.schema.json (schema_version 1.0). Usage is always reported with contracts/v1/usage-event.schema.json, audit with contracts/v1/audit-event.schema.json.
API endpoints
| Method | Path |
|---|---|
| GET | /v1/audit/correlations |
| GET | /v1/telemetry/traces/{trace_id} |
| GET | /v1/finops/usage |
GET /v1/audit/correlations
Response:
{
"items": [
{
"trace_id": "4bf92f35...",
"run_id": "run_42",
"events": 14,
"actions": [
"policy.evaluate",
"tool.invoke",
"model.invoke"
],
"decisions": {
"allow": 13,
"deny": 1
}
}
],
"next_cursor": null
}
GET /v1/telemetry/traces/{trace_id}
Response:
{
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"spans": [
{
"span_id": "00f067aa0ba902b7",
"name": "gen_ai.chat",
"duration_ms": 1820
}
],
"links": {
"opik": {
"url": "https://<opik-host>/default/redirect/projects?name=eai-platform",
"metadata_key": "eai.trace_id",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}
}
}
GET /v1/finops/usage
Response:
{
"items": [
{
"event_id": "use_01J",
"usage_type": "llm",
"cost": {
"measured": {
"amount": "0.000228",
"currency": "USD"
}
}
}
],
"next_cursor": null
}
SDK examples
Python
from enterprise_ai import EnterpriseAI
client = EnterpriseAI(base_url="<control-plane-url>", api_key="eai_live_xxx", tenant="acme", locale="en")
client.audit.correlations(run_id="run_42")
client.telemetry.trace("4bf92f3577b34da6a3ce929d0e0e4736")
client.finops.usage(agent_id="agt_ap", limit=100)
TypeScript
import { EnterpriseAI } from "@enterprise-ai/sdk";
const client = new EnterpriseAI({ baseUrl: "<control-plane-url>", apiKey: process.env.EAI_API_KEY, tenant: "acme", locale: "en" });
await client.audit.correlations({ run_id: "run_42" });
await client.telemetry.trace("4bf92f3577b34da6a3ce929d0e0e4736");
await client.finops.usage.list({ agent_id: "agt_ap", limit: 100 });
Java
EnterpriseAIClient client = EnterpriseAIClient.builder()
.baseUrl("<control-plane-url>")
.apiKey(System.getenv("EAI_API_KEY"))
.tenant("acme")
.locale(Locale.ENGLISH)
.build();
client.audit().correlations(Map.of("run_id", "run_42"));
client.telemetry().trace("4bf92f3577b34da6a3ce929d0e0e4736");
client.finops().listUsage(Map.of("agent_id", "agt_ap", "limit", 100));
Go
client, err := eai.NewClient(eai.WithBaseURL("<control-plane-url>"), eai.WithAPIKey(os.Getenv("EAI_API_KEY")), eai.WithTenant("acme"), eai.WithLocale("en"))
if err != nil { log.Fatal(err) }
ctx := context.Background()
_, err = client.Audit.Correlations(ctx, eai.WithQuery(map[string]string{"run_id": "run_42"}))
_, err = client.Telemetry.Trace(ctx, "4bf92f3577b34da6a3ce929d0e0e4736")
_, err = client.FinOps.ListUsage(ctx, eai.WithQuery(map[string]string{"agent_id": "agt_ap"}), eai.WithLimit(100))
Configuration
Environment variables: EAI_CLICKHOUSE_URL, EAI_S3_BUCKET.
Helm values keys (deploy/helm/enterprise-ai/values.yaml): controlPlane.config.
Security
- Respects RLS and the caller clearance: content of higher classification is summarized, not shown.
- Tenant resolved from the credential, never from the body; PostgreSQL row level security on every tenant table.
- Every mutation and policy decision writes a hash chained audit event (G-11).
- Errors use the standard envelope with message_i18n en and fr.
Observability
- Query latency of correlations.
- W3C traceparent propagated, X-Request-Id returned, spans carry eai.tenant_id, eai.project, eai.agent_id, eai.run_id.
FinOps units
- Correlation shows the measured cost per run from Usage Events.
Failure modes and resilience
- Missing trace ids (legacy clients) -> fallback on request_id and time window.
Standalone adoption
Auditors use it read-only through the auditor role.
Acceptance tests
- A run with one denied tool call shows decisions allow=13 deny=1 and the matching usage.
Audited access and decision evidence (kit section 11)
- Every read, search, verification and export of the audit trail (
/v1/audit/*,/v1/evidence/*) writes aneai.audit_access_logrow (who, which query, how many results) and its ownaudit.read,audit.search,audit.verify,audit.export.readoraudit.payload.decryptevent. - The PEP decisions recorded in the event carry
decision_id,policy_id,policy_version,engine,reasons,obligationsand, when the engine reports it,bundle_revision. - The
data(classification, DLP, masking, egress) andversions(model, prompt, tool, skill, plugin) fields are chained with the event.