Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/iterate/sqlfu/llms.txt

Use this file to discover all available pages before exploring further.

sqlfu emits one OpenTelemetry span per query through instrument.otel({tracer}). The span carries the query name, the parameterized SQL text, and the adapter system. Because TracerLike is a structural type, there is no peer dependency on @opentelemetry/api — you bring your own OTel SDK and pass a tracer.

Setup

src/db.ts
import {trace} from '@opentelemetry/api';
import {instrument} from 'sqlfu';

const tracer = trace.getTracer('my-service');
const client = instrument(baseClient, instrument.otel({tracer}));
Then call generated wrappers normally:
import {listPosts} from './sql/.generated/queries.sql.ts';

const posts = await listPosts(client, {limit: 20});
Each call produces one span. The span is active for the duration of the query, so child spans from your own code are nested inside it automatically.

Span attributes

AttributeValue
db.query.summaryThe generated query name, e.g. listPosts
db.query.textParameterized SQL with placeholders — values are in args, not interpolated into the text
db.system.nameThe adapter system, e.g. sqlite
If the query throws, instrument.otel() records the exception as a span event and sets the span status to ERROR.

No peer dependency

instrument.otel accepts a TracerLike — a structural type with only a startActiveSpan(name, fn) method. The real OTel Tracer from @opentelemetry/api satisfies this, as does any compatible tracer from other SDKs. sqlfu itself has no import of @opentelemetry/api.

Full setup example

Configure your OTel SDK with the exporter for your backend, then pass the tracer to instrument.otel:
src/observability.ts
import {OTLPTraceExporter} from '@opentelemetry/exporter-trace-otlp-http';
import {NodeTracerProvider, SimpleSpanProcessor} from '@opentelemetry/sdk-trace-node';
import {trace} from '@opentelemetry/api';
import {instrument} from 'sqlfu';

const provider = new NodeTracerProvider({
  spanProcessors: [
    new SimpleSpanProcessor(new OTLPTraceExporter({
      url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
    })),
  ],
});

provider.register();

const tracer = trace.getTracer('my-service');
export const client = instrument(baseClient, instrument.otel({tracer}));
Any OTLP backend works here — an OpenTelemetry Collector, Honeycomb, Grafana Tempo, New Relic, or Datadog APM. Only the exporter URL and headers change; the instrument.otel line stays identical.

Ad-hoc SQL

Generated wrappers carry a name automatically. Ad-hoc SQL has no generated name, but you can pass one yourself:
await client.all({
  sql: 'select count(*) as count from posts',
  args: [],
  name: 'countPosts',
});
That name becomes db.query.summary on the span.

Composing with error reporting

instrument.onError calls a reporter whenever a query throws, then always rethrows. Compose it with instrument.otel to get both OTel spans and error reporting:
import {instrument} from 'sqlfu';
import * as Sentry from '@sentry/node';

const client = instrument(
  baseClient,
  instrument.otel({tracer}),
  instrument.onError(({context, error}) => {
    Sentry.captureException(error, {
      tags: {'db.query.summary': context.query.name || 'sql'},
    });
  }),
);

Composition order

instrument(client, a, b, c) applies hooks outer-to-inner: a wraps b, which wraps c, which wraps the underlying adapter call. Putting instrument.otel first means the OTel span covers the full execution, including any error-reporter work. Putting it last means the span only covers the raw adapter call.
instrument.otel and instrument.onError are reference implementations, not a forever-stable API. The stable contract is the QueryExecutionHook type. Copy the helpers and edit them to match your team’s conventions.

Observability

The full hook model, plus recipes for Sentry, PostHog, and Datadog.

Runtime client

The shared client interface and how instrument wraps it.

Build docs developers (and LLMs) love