AIDB emits OpenTelemetry (OTel) traces, logs, and metrics for its own function calls, background workers, and SQL query execution. For the GUCs that turn this feature on and configure it, see Configuring AIDB parameters — OpenTelemetry tracing.
Schemas
When aidb.otel_client = 'database', AIDB writes traces, logs, and metrics into three tables in the aidb_otel schema:
aidb_otel.traces— one row per span.aidb_otel.logs— one row per log record.aidb_otel.metrics— one row per periodic metrics snapshot (aPeriodicReadercollection bundling every instrument at that point in time).
All three tables share a common set of columns, plus a few columns specific to each.
Common columns
Every row in all three tables carries these columns:
| Column | Type | Description |
|---|---|---|
id | bigint | Row identity. |
recorded_at | timestamptz | When the row was written. |
payload_json | jsonb | The item as an OTLP protojson Export{Metrics,Logs,Trace}ServiceRequest. Populated when aidb.otel_client_storage_format is json or both. |
payload_binary | bytea | The same item as raw OTLP protobuf wire bytes. Populated when aidb.otel_client_storage_format is binary or both. |
export_status | text | pending, exported, or failed. Only meaningful when the exporter worker is enabled — stays pending forever otherwise. |
export_attempts | integer | Number of export attempts made so far. |
next_export_attempt_at | timestamptz | Earliest time the exporter will retry this row. NULL once exported or permanently failed. |
last_export_error | text | Error message from the most recent failed export attempt. |
At least one of payload_json/payload_binary is always populated — see Storage format.
aidb_otel.traces — additional columns
Each row is exactly one span. These identifier and attribute columns are extracted from the span before either payload column is serialized, so they're available regardless of storage format, and querying them never requires parsing the JSON or protobuf payload.
| Column | Type | Description |
|---|---|---|
trace_id | text | 32-character lowercase hex trace ID. Indexed (traces_trace_id_idx). |
span_id | text | 16-character lowercase hex span ID. |
parent_span_id | text | 16-character lowercase hex parent span ID. NULL for a root span. |
pg_session_id | text | The db.postgresql.session_id attribute, when present. Partial index excluding NULL (traces_pg_session_id_idx). |
gen_ai_conversation_id | text | The gen_ai.conversation.id attribute, when present. Partial index excluding NULL (traces_gen_ai_conversation_id_idx). |
mcp_session_id | text | The mcp.session.id attribute, when present. Partial index excluding NULL (traces_mcp_session_id_idx). |
decision_status | text | The aidb.agent.decision.action_status attribute (allowed or denied), when present — only the one purpose_decision span per enforcement decision carries it. Partial index excluding NULL (traces_decision_status_idx). Lets a per-agent allowed/denied rollup over a time window use an index range scan instead of scanning payload_json. See Purpose-enforcement decisions. |
trace_id/span_id/parent_span_id are lowercase hex, the same representation payload_json's own traceId/spanId/parentSpanId fields use — the two match as strings, so you can look up a row by either. (This isn't the same as payload_binary's raw OTLP protobuf bytes fields, which aren't hex or text at all.)
aidb_otel.logs — additional columns
Each row is one log record, with two columns linking it back to the span it was emitted under:
| Column | Type | Description |
|---|---|---|
trace_id | text | Trace ID of the span this log record was emitted under. NULL if emitted outside any active span. Indexed (logs_trace_id_idx). |
span_id | text | Span ID of the span this log record was emitted under. NULL under the same condition as trace_id. |
Configuring storage format
The aidb.otel_client_storage_format GUC controls which payload column the database client (one of the aidb.otel_client destinations — see Configuring AIDB parameters) writes to: payload_json, payload_binary, or both — the two columns described under Common columns. Values:
| Value | payload_json | payload_binary | Notes |
|---|---|---|---|
json | ✅ | — | Default. Human-readable, directly queryable with standard jsonb operators. |
binary | — | ✅ | Raw OTLP protobuf. Cheaper to produce (no intermediate JSON tree), but not directly queryable. |
both | ✅ | ✅ | Both populated from the same underlying data. Roughly doubles storage and write cost. |
aidb.otel_client_storage_format only affects the database client (aidb.otel_client = 'database'). It has no effect on noop (nothing is written), stdout or grpc (both always emit the full OTLP payload directly, not through these columns), or log, which always writes payload_json regardless of this setting, since a server log line is a human-readable debugging aid, not storage a collector later forwards.
When exporting a row that has both payloads populated (storage_format = both), the exporter worker sends only payload_binary, since it's cheaper to have produced.
Triggering retention cleanup
The cleaner background worker deletes old rows automatically (see Running background workers), but this function lets you trigger a cleanup pass immediately instead of waiting for its next tick.
aidb.run_otel_retention_cleanup()
Manually triggers a full retention cleanup pass, deleting rows older than aidb.otel_cleaner_retention_days from all three aidb_otel tables, in the same bounded batches the cleaner background worker uses. Runs immediately, regardless of aidb.otel_cleaner_enabled — useful for an ops task or a test that shouldn't wait for the worker's own wake-up interval.
Requires the aidb_governance role (see Managing audit access) — aidb_users has no EXECUTE on this function.
Takes no arguments and returns void. Safe to call while the cleaner background worker is running — the two don't compete over the same rows.
SELECT aidb.run_otel_retention_cleanup();
Note
There's no equivalent manual trigger for the exporter. Forwarding to an external collector only ever runs from the exporter worker's own tick, so that a delivery in progress never shares a transaction with anything else that could roll it back and risk sending the same row twice.
Internally, the database client writes through aidb_otel.store_metrics(), aidb_otel.store_logs(), and aidb_otel.store_traces(). These aren't meant to be called directly — aidb_users has EXECUTE on them (needed for any traced call to work) but no SELECT on the tables they write to.
Running background workers
Two background workers run per database, each spawned only when enabled for that database (see Configuring AIDB parameters for the governing GUCs).
Exporting traces, logs, and metrics
Forwards pending, due rows from aidb_otel.{traces,logs,metrics} to the OTLP/HTTP collector at aidb.otel_exporter_endpoint, on a 15-second tick. payload_json rows are sent as OTLP/HTTP+JSON. payload_binary rows are sent as OTLP/HTTP+protobuf (Content-Type: application/x-protobuf) — a real OTel Collector's HTTP receiver dispatches on that header, so both formats share the same endpoint. Every outbound request goes through AIDB's egress allowlist.
Failed deliveries back off per row (not per tick), so one row's retry schedule never blocks the rest of the batch. After aidb.otel_exporter_max_retries attempts, a row is marked export_status = 'failed' and stops being retried automatically — find these with:
SELECT * FROM aidb_otel.traces WHERE export_status = 'failed';
Cleaning up expired rows
Deletes rows older than aidb.otel_cleaner_retention_days from all three tables, in batches of aidb.otel_cleaner_batch_size, on an hourly tick. Set aidb.otel_cleaner_retention_days = 0 to disable cleanup entirely without stopping the worker itself.
Traced attributes
AIDB attaches attributes to spans, following OpenTelemetry semantic conventions where applicable (gen_ai.*, db.*, error.type), plus custom aidb.* attributes for concepts with no standard equivalent. These attributes are recorded automatically whenever tracing is enabled — you don't set them yourself. They reflect what AIDB observes about each call.
Setting resource attributes
Set once per process, shared by every signal (traces, logs, and metrics):
| Attribute | Description |
|---|---|
service.version | The AIDB extension version that produced the data. |
aidb.database.name | Name of the database the backend is connected to. |
aidb.database.oid | OID of that database. |
Setting root-span attribute
Set once per trace, only on its root span:
| Attribute | Description |
|---|---|
db.postgresql.session_id | Correlates a trace with the Postgres server log lines from the same backend. See below for the format and where it's set. |
db.postgresql.session_id uses the format <start_time_hex>.<pid_hex>, matching Postgres's own %c log_line_prefix escape. It's per-connection, so it's set only on each trace's root span — not on every span, and not as a resource attribute — since sharing it with the metrics pipeline would inflate metric cardinality. It's promoted to the indexed pg_session_id column on aidb_otel.traces.
Recording Agent Hub attributes
Set on spans for agent conversations, tasks, and messages:
| Attribute | Description |
|---|---|
gen_ai.operation.name | Static operation label on agent conversation spans. |
gen_ai.agent.name | Agent name. |
gen_ai.agent.id | Agent ID. |
gen_ai.conversation.id | Conversation ID. Promoted to the indexed gen_ai_conversation_id column on aidb_otel.traces. |
aidb.agent.model | Model used by the agent. |
aidb.agent.purpose | The agent's declared purpose (see Permissions). |
aidb.agent.status | Outcome status of an agent call. |
aidb.agent.task.id | Task ID, for multi-step agent runs. |
aidb.agent.message.id | Message ID, within a conversation. |
aidb.read_only | Whether the call was read-only. Shared with Tools Hub — not agent-exclusive. |
error.type | Error classification on a failed call. |
Recording Tools Hub attributes
Set on spans for tool calls, including those imported from an MCP server:
| Attribute | Description |
|---|---|
gen_ai.tool.name | Tool name. |
aidb.mcp.server_name | Name of the MCP server a tool was imported from. |
aidb.mcp.url | MCP server URL. Recorded as-is — access to trace data is controlled by the aidb_governance role, not by redacting this value. |
aidb.mcp.transport | MCP transport type. |
aidb.tool.call_count | Number of tool calls in a batch invocation. |
mcp.session.id | MCP session ID, when a tool call happens within one. Promoted to the indexed mcp_session_id column on aidb_otel.traces. |
Recording purpose-enforcement decisions
Every tool SQL execution run under a purpose-resolved role emits exactly one purpose_decision span carrying these attributes:
| Attribute | Description |
|---|---|
aidb.agent.decision.decision_id | Unique ID for this decision. Also embedded in the error message text when the action is denied, so an error can be linked back to its decision record. |
aidb.agent.decision.action_status | allowed or denied — the only two values. Promoted to the indexed decision_status column on aidb_otel.traces. |
aidb.agent.decision.interaction_point | The kind of interaction being decided. Currently always sql_exec (SQL execution by a tool) — the only interaction point implemented so far. |
aidb.agent.decision.principal | The principal the decision was evaluated for. |
aidb.agent.decision.agent_role | The purpose-resolved Postgres role the action ran (or would have run) as. |
aidb.agent.decision.denial_reason | Present only when denied. One of role_not_found (the purpose's role doesn't exist), not_role_member (the caller isn't a member of the purpose's role), escalation_blocked (the SQL tried to change the running role with SET ROLE, RESET ROLE, or SET SESSION AUTHORIZATION, or to change a privilege-restricted setting), or engine_privilege_denied (Postgres rejected the SQL with a 42501 permission error). Not promoted to its own column. |
Tracing SQL queries
Set on any span that executes SQL:
| Attribute | Description |
|---|---|
db.query.text | The SQL statement(s) executed during the traced call. Bound parameter values are included only when aidb.otel_capture_query_parameters is on (see Configuring AIDB parameters). |
Tracing pipelines, semantic KBs, and models
These cover pipeline CRUD and execution, semantic KB CRUD and search, model inference (OCR, summarization, reranking, embedding, generation), data processors (chunking, PDF/HTML parsing), model maintenance, and volume operations.
These cover pipeline CRUD and execution, semantic KB CRUD and search, model inference (OCR, summarization, reranking, embedding, generation), data processors (chunking, PDF/HTML parsing), model maintenance, and volume operations.
These cover pipeline CRUD and execution, semantic KB CRUD and search, model inference (OCR, summarization, reranking, embedding, generation), data processors (chunking, PDF/HTML parsing), model maintenance, and volume operations.
These cover pipeline CRUD and execution, semantic KB CRUD and search, model inference (OCR, summarization, reranking, embedding, generation), data processors (chunking, PDF/HTML parsing), model maintenance, and volume operations.
| Attribute | Description |
|---|---|
aidb.pipeline.name | Pipeline name, on pipeline CRUD and execution spans. |
aidb.pipeline.id | Pipeline ID, on background worker batch-processing spans. |
aidb.semantic_kb.name | Semantic knowledge base name. |
aidb.model.name | Model name, on model inference and maintenance spans. |