The edb-endpoints server can also act as an OpenTelemetry Protocol over HTTP (OTLP/HTTP) receiver. Agents outside AIDB send their telemetry to it, and AIDB stores it in the same aidb_otel tables as its own telemetry, so one audit trail covers both. The receiver is off by default.
Enabling the receiver
In
postgresql.conf, turn the receiver on:edb.endpoints_otlp_enabled = on
Grant the
aidb_otel_writerrole to every database role that is allowed to send telemetry:GRANT aidb_otel_writer TO agent_alice;
Reload Postgres. If neither the MCP server nor the receiver was on when Postgres started, restart Postgres instead, because the supervisor starts only if a server switch is on at startup.
SELECT pg_reload_conf();
Check that the receiver is up by sending a test trace with the example under Sending telemetry. A
200means the receiver stored it. A401or403means it's listening but rejected the credentials or the role. See Responses.
The receiver listens on edb.endpoints_otlp_port (default 8766). It takes its host, TLS pair, and databases from the general edb.endpoints_* settings, never from the edb.endpoints_mcp_* ones. See Configuring the endpoint. The supervisor starts the receiver for you. To launch edb-endpoints yourself, see Launching edb-endpoints yourself.
Sending telemetry
Send telemetry to the URL for your database and signal. Each database the endpoint serves has its own URL, with the signal at the end:
POST http(s)://<host>:8766/otlp/<database>/v1/traces POST http(s)://<host>:8766/otlp/<database>/v1/logs POST http(s)://<host>:8766/otlp/<database>/v1/metrics
Point an OpenTelemetry SDK's OTLP/HTTP exporter at http(s)://<host>:8766/otlp/<database>, and it adds the /v1/<signal> part itself. There's no default database. A name that isn't in edb.endpoints_database gets a 404 with the body unknown database.
For example, to send a trace from a shell:
curl -sS -u agent_alice:<password> \ -H 'Content-Type: application/json' \ -d @trace.json \ http://127.0.0.1:8766/otlp/postgres/v1/traces
Each request must meet these requirements:
- Formats:
Content-Type: application/x-protobuforapplication/json, optionally withContent-Encoding: gzip. A body is at most 20 MiB, compressed or decompressed. - Authentication: HTTP Basic with a Postgres role's own credentials, or a bearer token when OAuth is on. Each request opens its own connection as that role, under the same
pg_hba.confrules as the MCP server. - Authorization: the role must be a member of
aidb_otel_writer.
The receiver then handles the request as follows:
- Storage: the receiver calls the matching
aidb_otel.ingest_*function, which stores one row per span or log record, labels the rowsexternal, and records the role asprincipal. The receiver replies after the commit, so an exporter that retries doesn't create duplicates. - Load: at most 16 requests upload, decode, or hold a database connection at a time, separately from MCP. An upload that takes more than 30 seconds is cut off. Requests beyond the limit get a retryable
503withRetry-After, so a burst of telemetry can't use upmax_connectionsor slow down MCP.
Responses
| Status | When | What to do |
|---|---|---|
| 200 | Stored. The body is an empty Export*ServiceResponse in the request's format. | Nothing. |
| 400 | Invalid gzip or JSON, or a request AIDB can't decode. The error message names the failing element, such as payloads[1]. | Fix the request body. Don't resend it unchanged. |
| 401 | Missing or wrong credentials, a refused bearer token, or a pg_hba.conf line that doesn't check the credential. | Check the password, or fetch a new token. Make sure the role's pg_hba.conf line checks a password or token. |
| 403 | The role isn't a member of aidb_otel_writer. | Run GRANT aidb_otel_writer TO <role>;. |
| 404 | Unlisted database, unknown signal, or a database without the current AIDB extension. | Use a database listed in edb.endpoints_database, a valid signal, and a database that has the current AIDB extension. |
| 408 | The body didn't finish uploading within 30 seconds. | Resend over a faster connection, or send smaller batches. |
| 413 | The body is larger than 20 MiB, compressed or decompressed. | Send smaller batches. |
| 415 | Unsupported Content-Type or Content-Encoding. | Use application/x-protobuf or application/json, with gzip encoding or none. |
| 503 | Over the concurrency limit, or the database is unreachable, busy, restarting, or rolled the request back. Retryable, with Retry-After. | Retry after the interval in the Retry-After header. |
Error bodies are a google.rpc.Status in the request's format, as OTLP/HTTP specifies.
How AIDB stores received telemetry
The receiver passes each request to an aidb_otel.ingest_* function. You can also call these functions directly. Only members of the aidb_otel_writer role can run them, and the role has no read access to the tables. Grant it to each role that's allowed to send telemetry:
GRANT aidb_otel_writer TO agent_alice;
Each array element is one complete OTLP request. The functions behave as follows:
- A request is split into one row per span or log record. A metrics request stays one row.
- Rows keep the format they arrived in: the JSON functions fill only
payload_json, and the binary ones onlypayload_binary.aidb.otel_client_storage_formatdoesn't apply. - Rows are labeled
source = 'external', andprincipalis the role that called the function. - A span without a trace ID or span ID is skipped with a warning. A malformed, empty, or
NULLelement fails the whole call, and the error names it, such aspayloads[2] .... - A span repeated by the same writer is stored once, and isn't counted again.
- Each function returns the number of rows it stored.
Decision columns such as decision_status are stored as the sender reported them. To count only decisions AIDB enforced, filter on source = 'internal'.
Note
Rows stored before the 7.9.0 upgrade are labeled internal and have no principal, but AIDB can't prove that label. Earlier releases let any member of aidb_users write rows directly and recorded nothing that tells them apart.
Serving remote senders
To receive telemetry from other hosts, set edb.endpoints_host to a routable address, and give the endpoint a certificate and key with edb.endpoints_tls_cert and edb.endpoints_tls_key. Then reload with SELECT pg_reload_conf();. The receiver refuses plaintext HTTP off loopback (127.0.0.1, ::1, or localhost).
Note
The receiver ignores the edb.endpoints_mcp_* host and TLS settings. A non-loopback MCP host with an MCP-only TLS pair therefore still needs the general pair before the receiver can listen off loopback.
Authenticating with OAuth
Besides HTTP Basic, the receiver accepts a bearer token that your identity provider issued. The receiver uses the general edb.endpoints_oauth_* settings and needs the issuer and the audience. For the requirements, the settings, and the pg_hba.conf lines, see Authenticating with OAuth.
How the receiver handles a token:
- A bearer token must be a JSON Web Token (JWT). The receiver checks its shape, role claim, expiry, and
aud, then logs in to Postgres as the role the claim names. That role is the stored rows'principal, so it still needsaidb_otel_writer. - A request with no usable credentials gets a
401that offers bothBasic realm="otlp"andBearer. - A token that fails the check, or that Postgres refuses, gets a
401withBearer error="invalid_token", so the exporter knows to fetch a new token. - HTTP Basic keeps working alongside bearer tokens.
The sender, not AIDB, obtains the token and attaches it to each request as an Authorization: Bearer <token> header. AIDB has no setting for this. To attach it, set the standard OTEL_EXPORTER_OTLP_HEADERS environment variable, which belongs to OpenTelemetry and which most SDKs read, or use your SDK's equivalent option. Set it in the environment of the agent process that sends the telemetry, not on the Postgres server:
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer <token>"
Some SDKs expect the value percent-encoded, such as Authorization=Bearer%20<token>. See your SDK's documentation for the exact format. Tokens expire, and an exporter that reads a static header doesn't refresh them, so your application has to update the header before the token expires. When it doesn't, the receiver returns 401 with error="invalid_token".