Skip to main content

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​

MethodPath
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​

  1. 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 an eai.audit_access_log row (who, which query, how many results) and its own audit.read, audit.search, audit.verify, audit.export.read or audit.payload.decrypt event.
  • The PEP decisions recorded in the event carry decision_id, policy_id, policy_version, engine, reasons, obligations and, when the engine reports it, bundle_revision.
  • The data (classification, DLP, masking, egress) and versions (model, prompt, tool, skill, plugin) fields are chained with the event.

Implementation roadmap