# OpenTelemetry

`okmodel/otel` is experimental. It turns [observers](/docs/define/connect/observers) and [`onRoute`](/docs/define/connect/topology) 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.

```ts title="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.
