Skip to main content

Overview

@kubiks/otel-drizzle provides comprehensive OpenTelemetry instrumentation for Drizzle ORM. Add distributed tracing to your database queries with a single line of code—supports PostgreSQL, MySQL, and SQLite.
Drizzle ORM Trace Visualization
Visualize your database queries with detailed span information including operation type, SQL statements, and performance metrics.

Installation

npm install @kubiks/otel-drizzle
Peer Dependencies: @opentelemetry/api >= 1.9.0, drizzle-orm >= 0.28.0

Supported Frameworks

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

Next.js

App Router & Pages Router

Fastify

High-performance server

NestJS

Enterprise framework

Express

Classic Node.js server

Remix

Full-stack framework

SvelteKit

Modern web framework

Supported Platforms

Works with any observability platform that supports OpenTelemetry:

Quick Start

Use instrumentDrizzleClient() to add tracing to your Drizzle database instance:
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);
This is the simplest and most straightforward approach—just wrap your existing Drizzle instance!

Database-Specific Setup

  • PostgreSQL
  • MySQL
  • SQLite
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,
});
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" });

Configuration Options

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
});
By default, SQL queries are captured in spans. You can disable this by setting captureQueryText: false for sensitive environments.

What You Get

Each database query automatically creates a span with rich telemetry data:
  • 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.)
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
  • Exceptions are recorded with stack traces
  • Proper span status (OK, ERROR)
  • Error messages and types
  • Duration and timing information for every query
  • Query execution time
  • Database connection time

Span Attributes

The instrumentation adds the following attributes to each span following OpenTelemetry semantic conventions:
AttributeDescriptionExample
db.operationSQL operation typeSELECT
db.statementFull SQL queryselect "id", "name" from "users"...
db.systemDatabase systempostgresql
db.nameDatabase namemyapp
operation.nameClient operation namekubiks_otel-drizzle.client

Usage Examples

Basic Queries

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));

Transactions

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",
  });
});
Transaction queries are automatically marked with db.transaction attribute.

Complex Queries

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));

Row Level Security (PostgreSQL)

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:
lib/db.ts
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,
});
db/schema.ts
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(),
});
app/api/users/route.ts
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

Always use connection pooling in production:
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);
Disable SQL capture in production if queries contain sensitive data:
instrumentDrizzleClient(db, {
  captureQueryText: process.env.NODE_ENV !== "production",
  maxQueryTextLength: 500,
});
Always use transactions for multi-step operations:
await db.transaction(async (tx) => {
  const [user] = await tx.insert(users).values(userData).returning();
  await tx.insert(posts).values({ userId: user.id, ...postData });
});
Monitor slow queries in your traces and add indexes:
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_user_id ON posts(user_id);

Performance Considerations

The instrumentation adds minimal overhead (~1-2ms per query) for tracing operations.
Use OpenTelemetry sampling to reduce data volume in high-traffic applications:
import { TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";

const sdk = new NodeSDK({
  sampler: new TraceIdRatioBasedSampler(0.1), // Sample 10% of traces
});
Use batch span processors for better performance:
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";

const sdk = new NodeSDK({
  spanProcessor: new BatchSpanProcessor(exporter),
});

Troubleshooting

Ensure OpenTelemetry is initialized before instrumenting Drizzle:
import { NodeSDK } from "@opentelemetry/sdk-node";

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

sdk.start();

// Then instrument Drizzle
import { db } from "@/lib/db";
instrumentDrizzleClient(db);
Check that captureQueryText is enabled:
instrumentDrizzleClient(db, {
  captureQueryText: true,
  maxQueryTextLength: 2000,
});
Make sure you’re using Drizzle’s transaction API correctly:
// 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`);

Resources

License

MIT