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.

FieldPass it to
hookmconnect({ hookm }). Transactions and errors the caller does not receive.
onRouteconnect({ onRoute }). A topology calls it after it chooses an endpoint.
queryCall 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.

SpanNameAttributes
Routeread, write, tx, or lockdb.operation.name is that same word. The endpoint and the reason are a span event named route.
Transactiontransaction, or savepoint when the depth is above 0db.operation.name matches the name. Commit sets status OK. Rollback sets status ERROR and error.type to rollback.
Errorerrorerror.type is the error's name. Status is ERROR.
inspect()find notes, and the same shape for the other readsdb.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.

db.ts
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.

On this page