> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kubiks.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Drizzle ORM

> OpenTelemetry instrumentation for Drizzle ORM database queries

## Overview

`@kubiks/otel-drizzle` provides comprehensive OpenTelemetry instrumentation for [Drizzle ORM](https://orm.drizzle.team/). Add distributed tracing to your database queries with a single line of code—supports PostgreSQL, MySQL, and SQLite.

<Frame>
  <img src="https://mintcdn.com/kubiks-fb8cce26/yWvtWvPdTXh77mDC/images/otel/otel-drizzle-trace.png?fit=max&auto=format&n=yWvtWvPdTXh77mDC&q=85&s=cb8370dd17834eecd8bafded4edd67a1" alt="Drizzle ORM Trace Visualization" width="3379" height="2386" data-path="images/otel/otel-drizzle-trace.png" />
</Frame>

<Note>
  Visualize your database queries with detailed span information including operation type, SQL statements, and performance metrics.
</Note>

## Installation

<CodeGroup>
  ```bash npm theme={null}
  npm install @kubiks/otel-drizzle
  ```

  ```bash pnpm theme={null}
  pnpm add @kubiks/otel-drizzle
  ```

  ```bash yarn theme={null}
  yarn add @kubiks/otel-drizzle
  ```
</CodeGroup>

<Warning>
  **Peer Dependencies:** `@opentelemetry/api` >= 1.9.0, `drizzle-orm` >= 0.28.0
</Warning>

## Supported Frameworks

Works with any TypeScript framework and Node.js runtime that Drizzle supports:

<CardGroup cols={3}>
  <Card title="Next.js" icon="react">
    App Router & Pages Router
  </Card>

  <Card title="Fastify" icon="bolt">
    High-performance server
  </Card>

  <Card title="NestJS" icon="n">
    Enterprise framework
  </Card>

  <Card title="Express" icon="e">
    Classic Node.js server
  </Card>

  <Card title="Remix" icon="music">
    Full-stack framework
  </Card>

  <Card title="SvelteKit" icon="fire">
    Modern web framework
  </Card>
</CardGroup>

## Supported Platforms

Works with any observability platform that supports OpenTelemetry:

* [Kubiks](https://kubiks.ai)
* [Sentry](https://sentry.io)
* [Axiom](https://axiom.co)
* [Datadog](https://www.datadoghq.com)
* [New Relic](https://newrelic.com)
* [SigNoz](https://signoz.io)
* And many more...

## Quick Start

Use `instrumentDrizzleClient()` to add tracing to your Drizzle database instance:

```typescript theme={null}
import { drizzle } from "drizzle-orm/postgres-js";
import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";

// Create your Drizzle database instance as usual
const db = drizzle(process.env.DATABASE_URL!);

// Add instrumentation with a single line
instrumentDrizzleClient(db);

// That's it! All queries are now traced automatically
const users = await db.select().from(usersTable);
```

<Tip>
  This is the simplest and most straightforward approach—just wrap your existing Drizzle instance!
</Tip>

## Database-Specific Setup

<Tabs>
  <Tab title="PostgreSQL">
    <Accordion title="postgres.js">
      ```typescript theme={null}
      import { drizzle } from "drizzle-orm/postgres-js";
      import postgres from "postgres";
      import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";

      // Using connection string directly
      const db = drizzle(process.env.DATABASE_URL!);
      instrumentDrizzleClient(db, { dbSystem: "postgresql" });

      // Or with a client instance
      const queryClient = postgres(process.env.DATABASE_URL!);
      const db = drizzle({ client: queryClient });
      instrumentDrizzleClient(db, {
        dbSystem: "postgresql",
        dbName: "myapp",
        peerName: "db.example.com",
        peerPort: 5432,
      });
      ```
    </Accordion>

    <Accordion title="node-postgres (pg)">
      ```typescript theme={null}
      import { drizzle } from "drizzle-orm/node-postgres";
      import { Pool } from "pg";
      import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";

      // Using connection string directly
      const db = drizzle(process.env.DATABASE_URL!);
      instrumentDrizzleClient(db, { dbSystem: "postgresql" });

      // Or with a pool instance
      const pool = new Pool({ 
        connectionString: process.env.DATABASE_URL 
      });
      const db = drizzle({ client: pool });
      instrumentDrizzleClient(db, { dbSystem: "postgresql" });
      ```
    </Accordion>
  </Tab>

  <Tab title="MySQL">
    ```typescript theme={null}
    import { drizzle } from "drizzle-orm/mysql2";
    import mysql from "mysql2/promise";
    import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";

    // Using connection string directly
    const db = drizzle(process.env.DATABASE_URL!);
    instrumentDrizzleClient(db, { dbSystem: "mysql" });

    // Or with a connection instance
    const connection = await mysql.createConnection({
      host: "localhost",
      user: "root",
      database: "mydb",
      // ... other connection options
    });
    const db = drizzle({ client: connection });
    instrumentDrizzleClient(db, {
      dbSystem: "mysql",
      dbName: "mydb",
      peerName: "localhost",
      peerPort: 3306,
    });
    ```
  </Tab>

  <Tab title="SQLite">
    <Accordion title="better-sqlite3">
      ```typescript theme={null}
      import { drizzle } from "drizzle-orm/better-sqlite3";
      import Database from "better-sqlite3";
      import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";

      // Using file path directly
      const db = drizzle("sqlite.db");
      instrumentDrizzleClient(db, { dbSystem: "sqlite" });

      // Or with a Database instance
      const sqlite = new Database("sqlite.db");
      const db = drizzle({ client: sqlite });
      instrumentDrizzleClient(db, { dbSystem: "sqlite" });
      ```
    </Accordion>

    <Accordion title="LibSQL/Turso">
      ```typescript theme={null}
      import { drizzle } from "drizzle-orm/libsql";
      import { createClient } from "@libsql/client";
      import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";

      // Using connection config directly
      const db = drizzle({
        connection: {
          url: process.env.DATABASE_URL!,
          authToken: process.env.DATABASE_AUTH_TOKEN,
        }
      });
      instrumentDrizzleClient(db, { dbSystem: "sqlite" });

      // Or with a client instance
      const client = createClient({
        url: process.env.DATABASE_URL!,
        authToken: process.env.DATABASE_AUTH_TOKEN,
      });
      const db = drizzle({ client });
      instrumentDrizzleClient(db, { dbSystem: "sqlite" });
      ```
    </Accordion>
  </Tab>
</Tabs>

## Configuration Options

```typescript theme={null}
instrumentDrizzleClient(db, {
  dbSystem: "postgresql",    // Database type: 'postgresql' | 'mysql' | 'sqlite'
  dbName: "myapp",           // Database name for spans
  captureQueryText: true,    // Include SQL in traces (default: true)
  maxQueryTextLength: 1000,  // Max SQL length (default: 1000)
  peerName: "db.example.com", // Database server hostname
  peerPort: 5432,           // Database server port
});
```

<Info>
  By default, SQL queries are captured in spans. You can disable this by setting `captureQueryText: false` for sensitive environments.
</Info>

## What You Get

Each database query automatically creates a span with rich telemetry data:

<AccordionGroup>
  <Accordion title="Span Information">
    * **Span name**: `drizzle.select`, `drizzle.insert`, `drizzle.update`, etc.
    * **Operation type**: `db.operation` attribute (SELECT, INSERT, UPDATE, DELETE, SET)
    * **SQL query text**: Full query statement captured in `db.statement` (configurable)
    * **Database system**: `db.system` attribute (postgresql, mysql, sqlite, etc.)
  </Accordion>

  <Accordion title="Transaction Support">
    All queries within transactions are automatically traced, including:

    * RLS (Row Level Security) queries like `SET LOCAL role` and `set_config()`
    * All nested transaction queries
    * Transaction rollbacks and commits
  </Accordion>

  <Accordion title="Error Tracking">
    * Exceptions are recorded with stack traces
    * Proper span status (OK, ERROR)
    * Error messages and types
  </Accordion>

  <Accordion title="Performance Metrics">
    * Duration and timing information for every query
    * Query execution time
    * Database connection time
  </Accordion>
</AccordionGroup>

## Span Attributes

The instrumentation adds the following attributes to each span following [OpenTelemetry semantic conventions](https://opentelemetry.io/docs/specs/semconv/database/):

| Attribute        | Description           | Example                               |
| ---------------- | --------------------- | ------------------------------------- |
| `db.operation`   | SQL operation type    | `SELECT`                              |
| `db.statement`   | Full SQL query        | `select "id", "name" from "users"...` |
| `db.system`      | Database system       | `postgresql`                          |
| `db.name`        | Database name         | `myapp`                               |
| `operation.name` | Client operation name | `kubiks_otel-drizzle.client`          |

## Usage Examples

### Basic Queries

<CodeGroup>
  ```typescript Select theme={null}
  import { db } from "@/lib/db";
  import { users } from "@/db/schema";

  // Traced as: drizzle.select
  const allUsers = await db.select().from(users);

  // With conditions
  const activeUsers = await db
    .select()
    .from(users)
    .where(eq(users.active, true));
  ```

  ```typescript Insert theme={null}
  import { db } from "@/lib/db";
  import { users } from "@/db/schema";

  // Traced as: drizzle.insert
  await db.insert(users).values({
    name: "John Doe",
    email: "john@example.com",
  });

  // Multiple rows
  await db.insert(users).values([
    { name: "Alice", email: "alice@example.com" },
    { name: "Bob", email: "bob@example.com" },
  ]);
  ```

  ```typescript Update theme={null}
  import { db } from "@/lib/db";
  import { users } from "@/db/schema";

  // Traced as: drizzle.update
  await db
    .update(users)
    .set({ active: false })
    .where(eq(users.id, userId));
  ```

  ```typescript Delete theme={null}
  import { db } from "@/lib/db";
  import { users } from "@/db/schema";

  // Traced as: drizzle.delete
  await db
    .delete(users)
    .where(eq(users.id, userId));
  ```
</CodeGroup>

### Transactions

```typescript theme={null}
import { db } from "@/lib/db";
import { users, posts } from "@/db/schema";

// All queries within the transaction are traced
await db.transaction(async (tx) => {
  const [user] = await tx
    .insert(users)
    .values({ name: "John" })
    .returning();

  await tx.insert(posts).values({
    userId: user.id,
    title: "First Post",
  });
});
```

<Note>
  Transaction queries are automatically marked with `db.transaction` attribute.
</Note>

### Complex Queries

<CodeGroup>
  ```typescript Joins theme={null}
  import { db } from "@/lib/db";
  import { users, posts } from "@/db/schema";

  // Traced with full SQL statement
  const usersWithPosts = await db
    .select({
      user: users,
      post: posts,
    })
    .from(users)
    .leftJoin(posts, eq(users.id, posts.userId));
  ```

  ```typescript Aggregations theme={null}
  import { db } from "@/lib/db";
  import { posts } from "@/db/schema";
  import { count } from "drizzle-orm";

  // Traced with aggregation query
  const postCounts = await db
    .select({
      userId: posts.userId,
      count: count(),
    })
    .from(posts)
    .groupBy(posts.userId);
  ```

  ```typescript Subqueries theme={null}
  import { db } from "@/lib/db";
  import { users, posts } from "@/db/schema";

  // Complex queries are fully traced
  const activeUsersSubquery = db
    .select({ id: users.id })
    .from(users)
    .where(eq(users.active, true));

  const postsFromActiveUsers = await db
    .select()
    .from(posts)
    .where(inArray(posts.userId, activeUsersSubquery));
  ```
</CodeGroup>

### Row Level Security (PostgreSQL)

```typescript theme={null}
import { db } from "@/lib/db";
import { users } from "@/db/schema";

// RLS queries are automatically traced
await db.transaction(async (tx) => {
  // SET LOCAL role is traced
  await tx.execute(sql`SET LOCAL role = 'authenticated'`);
  await tx.execute(sql`SELECT set_config('request.jwt.claim.sub', '${userId}', true)`);
  
  // Regular queries with RLS applied
  const userPosts = await tx
    .select()
    .from(posts)
    .where(eq(posts.userId, userId));
});
```

## Complete Integration Example

Here's a complete example of Drizzle ORM with OpenTelemetry in a Next.js application:

```typescript lib/db.ts theme={null}
import { drizzle } from "drizzle-orm/postgres-js";
import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";
import * as schema from "@/db/schema";

// Create Drizzle instance
export const db = drizzle(process.env.DATABASE_URL!, { schema });

// Instrument for tracing
instrumentDrizzleClient(db, {
  dbSystem: "postgresql",
  dbName: "myapp",
  captureQueryText: true,
  maxQueryTextLength: 2000,
});
```

```typescript db/schema.ts theme={null}
import { pgTable, serial, text, boolean, timestamp } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  active: boolean("active").default(true),
  createdAt: timestamp("created_at").defaultNow(),
});

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  userId: serial("user_id").references(() => users.id),
  title: text("title").notNull(),
  content: text("content"),
  createdAt: timestamp("created_at").defaultNow(),
});
```

```typescript app/api/users/route.ts theme={null}
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { users } from "@/db/schema";
import { eq } from "drizzle-orm";

export async function GET() {
  // Automatically traced
  const allUsers = await db.select().from(users);
  return NextResponse.json(allUsers);
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  
  // Automatically traced
  const [newUser] = await db
    .insert(users)
    .values(body)
    .returning();
  
  return NextResponse.json(newUser);
}
```

## Best Practices

<AccordionGroup>
  <Accordion title="Use Connection Pooling">
    Always use connection pooling in production:

    ```typescript theme={null}
    import { Pool } from "pg";
    import { drizzle } from "drizzle-orm/node-postgres";

    const pool = new Pool({
      connectionString: process.env.DATABASE_URL,
      max: 20, // Maximum pool size
      idleTimeoutMillis: 30000,
      connectionTimeoutMillis: 2000,
    });

    export const db = drizzle({ client: pool });
    instrumentDrizzleClient(db);
    ```
  </Accordion>

  <Accordion title="Configure SQL Capture Wisely">
    Disable SQL capture in production if queries contain sensitive data:

    ```typescript theme={null}
    instrumentDrizzleClient(db, {
      captureQueryText: process.env.NODE_ENV !== "production",
      maxQueryTextLength: 500,
    });
    ```
  </Accordion>

  <Accordion title="Use Transactions for Data Integrity">
    Always use transactions for multi-step operations:

    ```typescript theme={null}
    await db.transaction(async (tx) => {
      const [user] = await tx.insert(users).values(userData).returning();
      await tx.insert(posts).values({ userId: user.id, ...postData });
    });
    ```
  </Accordion>

  <Accordion title="Index Your Queries">
    Monitor slow queries in your traces and add indexes:

    ```sql theme={null}
    CREATE INDEX idx_users_email ON users(email);
    CREATE INDEX idx_posts_user_id ON posts(user_id);
    ```
  </Accordion>
</AccordionGroup>

## Performance Considerations

<AccordionGroup>
  <Accordion title="Minimal Overhead">
    The instrumentation adds minimal overhead (\~1-2ms per query) for tracing operations.
  </Accordion>

  <Accordion title="Sampling">
    Use OpenTelemetry sampling to reduce data volume in high-traffic applications:

    ```typescript theme={null}
    import { TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";

    const sdk = new NodeSDK({
      sampler: new TraceIdRatioBasedSampler(0.1), // Sample 10% of traces
    });
    ```
  </Accordion>

  <Accordion title="Batch Span Processing">
    Use batch span processors for better performance:

    ```typescript theme={null}
    import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";

    const sdk = new NodeSDK({
      spanProcessor: new BatchSpanProcessor(exporter),
    });
    ```
  </Accordion>
</AccordionGroup>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Spans Not Appearing">
    Ensure OpenTelemetry is initialized before instrumenting Drizzle:

    ```typescript theme={null}
    import { NodeSDK } from "@opentelemetry/sdk-node";

    const sdk = new NodeSDK({
      // ... configuration
    });

    sdk.start();

    // Then instrument Drizzle
    import { db } from "@/lib/db";
    instrumentDrizzleClient(db);
    ```
  </Accordion>

  <Accordion title="SQL Not Captured">
    Check that `captureQueryText` is enabled:

    ```typescript theme={null}
    instrumentDrizzleClient(db, {
      captureQueryText: true,
      maxQueryTextLength: 2000,
    });
    ```
  </Accordion>

  <Accordion title="Transaction Queries Not Grouped">
    Make sure you're using Drizzle's transaction API correctly:

    ```typescript theme={null}
    // Correct: All queries in callback are grouped
    await db.transaction(async (tx) => {
      await tx.insert(users).values(data);
    });

    // Incorrect: Manual transaction control not supported
    await db.execute(sql`BEGIN`);
    await db.insert(users).values(data);
    await db.execute(sql`COMMIT`);
    ```
  </Accordion>
</AccordionGroup>

## Resources

<CardGroup cols={2}>
  <Card title="Drizzle ORM Documentation" icon="book" href="https://orm.drizzle.team/docs/overview">
    Learn more about Drizzle ORM
  </Card>

  <Card title="GitHub Repository" icon="github" href="https://github.com/kubiks-inc/otel/tree/main/packages/otel-drizzle">
    View source code and examples
  </Card>

  <Card title="npm Package" icon="box" href="https://www.npmjs.com/package/@kubiks/otel-drizzle">
    View package on npm
  </Card>

  <Card title="Report Issues" icon="circle-exclamation" href="https://github.com/kubiks-inc/otel/issues">
    Found a bug? Let us know!
  </Card>
</CardGroup>

## License

MIT
