OpenTelemetry
okmodel/otel turns the existing observers into db.* spans.
okmodel/otel is experimental. It turns observers and onRoute into OpenTelemetry spans. You pass the tracer. The package does not import @opentelemetry/*.
otel({ tracer }) returns three things.
| Field | Pass it to |
|---|---|
hookm | connect({ hookm }). Transactions and errors the caller does not receive. |
onRoute | connect({ onRoute }). A topology calls it after it chooses an endpoint. |
query | Call it with inspect(). That is the fingerprint. |
A string or a pool never calls onRoute. Routing spans need { primary, replicas }.
Attributes
Every span sets db.system.name to postgresql. The span kind is client.
| Span | Name | Attributes |
|---|---|---|
| Route | read, write, tx, or lock | db.operation.name is that same word. The endpoint and the reason are a span event named route. |
| Transaction | transaction, or savepoint when the depth is above 0 | db.operation.name matches the name. Commit sets status OK. Rollback sets status ERROR and error.type to rollback. |
| Error | error | error.type is the error's name. Status is ERROR. |
inspect() | find notes, and the same shape for the other reads | db.operation.name is the call. db.collection.name is the table. db.query.summary is the fingerprint. |
onRoute fires once, so that span starts and ends in the same call. It has no duration. The route event does not carry a fingerprint, a table, or statement text. query(inspect()) is what sets db.query.summary.
Statement text is off. statement: true sets db.query.text from that inspection. Parameters are never attributes. A tenant id is not an attribute.
onError is an error the caller does not receive, such as one thrown from afterCommit. A query that throws to the caller is not that event. The exception is the error name and the OKM code when the error has one, with no message and no stack.
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { otel, type OtelSpan, type OtelTracer } from "okmodel/otel";
const notes = table("notes", {
id: t.identity(),
title: t.text(),
});
const app = schema({ tables: [notes] });
const tracer: OtelTracer = {
startSpan() {
const span: OtelSpan = {
setAttribute() {},
setStatus() {},
recordException() {},
addEvent() {},
end() {},
};
return span;
},
};
const tracing = otel({ tracer });
export const db = await connect(
{
primary: process.env.DATABASE_URL!,
replicas: [process.env.REPLICA_URL!],
},
{
schema: app,
hookm: [tracing.hookm],
onRoute: tracing.onRoute,
},
);
const pending = db.notes.find({ limit: 50 });
tracing.query(await pending.inspect());
await pending;Pass statement: true to otel when the span should carry db.query.text. The parameters still stay off.