Query Watcher for NestJS
This guide explains how to integrate the Lens Query Watcher into your NestJS application. The Query Watcher allows you to monitor and log database queries, providing valuable insights into your application's data access patterns and performance.
Lens provides built-in handlers for popular NestJS database integrations.
1. Sequelize
To integrate Sequelize with the Query Watcher, follow these steps:
Installation: Ensure you have the NestJS Sequelize package installed as per the official NestJS documentation.
Configure Sequelize Logging: In your
database.providers.tsfile (or wherever you configure Sequelize), you need to enablebenchmarkandlogQueryParametersand configure theloggingoption to emitsequelizeQueryevents.tsimport { Sequelize } from 'sequelize-typescript'; import { watcherEmitter } from '@lensjs/watchers'; import { TestModel } from './models/user.model.js'; export const databaseProviders = [ { provide: 'SEQUELIZE', useFactory: async () => { const sequelize = new Sequelize({ dialect: 'mysql', host: 'localhost', port: 3306, username: process.env.DATABASE_USERNAME, password: process.env.DATABASE_PASSWORD, database: process.env.DATABASE_NAME, benchmark: true, // Essential for logging query timings logQueryParameters: true, // Essential for logging query parameters logging: (sql: string, timing?: number) => { // Emit the 'sequelizeQuery' event with SQL and timing data watcherEmitter.emit('sequelizeQuery', { sql, timing }); }, }); sequelize.addModels([TestModel]); await sequelize.sync(); return sequelize; }, }, ];Integrate with
main.ts: In yourmain.tsfile, importcreateSequelizeHandlerfrom@lensjs/watchersand pass it to thequeryWatcherconfiguration within thelensfunction. Specify the database provider you are using (e.g.,'mysql','sqlite').tsimport { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module.js'; import { lens } from '@lensjs/nestjs'; import { createSequelizeHandler } from '@lensjs/watchers'; async function bootstrap() { const app = await NestFactory.create(AppModule); await lens({ app, queryWatcher: { enabled: true, handler: createSequelizeHandler({ provider: 'mysql' }), // Replace 'mysql' with your database provider }, }); await app.listen(process.env.PORT ?? 3000); } bootstrap();
2. Prisma
To integrate Prisma with the Query Watcher, follow these steps:
Installation: Begin by following the official NestJS Prisma integration guide to set up Prisma in your application.
Configure PrismaService Logging: When creating your
PrismaService, it's crucial to pass thelogoption with'query'to thesupermethod. This enables Prisma to emit query events that the Query Watcher can capture.tsimport { Injectable, OnModuleInit } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit { constructor() { // This is essential for Prisma to emit query events super({ log: ['query'], }); } async onModuleInit() { await this.$connect(); } }Integrate with
main.ts: In yourmain.tsfile, retrieve thePrismaServiceinstance from the application context. Then, usecreatePrismaHandlerfrom@lensjs/watchersand pass both theprismainstance and your databaseprovider(e.g.,'sqlite','postgresql') to thequeryWatcherconfiguration within thelensfunction.tsimport { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module.js'; import { lens } from '@lensjs/nestjs'; import { PrismaService } from './prisma.service.js'; import { createPrismaHandler } from '@lensjs/watchers'; async function bootstrap() { const app = await NestFactory.create(AppModule); const prisma = app.get(PrismaService); await lens({ app, queryWatcher: { enabled: true, handler: createPrismaHandler({ prisma, provider: 'sqlite' }), // Replace 'sqlite' with your database provider }, }); await app.listen(process.env.PORT ?? 3000); } bootstrap();
3. Kysely
To integrate Kysely with the Query Watcher, follow these steps:
Installation: Install a package that integrates Kysely with NestJS, such as nestjs-kysely.
Configure Kysely Logging: In your Kysely module configuration, set up the
logoption to emitkyselyQueryevents usingwatcherEmitter.emit.tsimport { watcherEmitter } from '@lensjs/watchers'; import { Module } from '@nestjs/common'; import { KyselyModule as BaseKyselyModule } from 'nestjs-kysely'; @Module({ imports: [ BaseKyselyModule.forRoot({ // ... other configurations log: (event) => { // Emit the 'kyselyQuery' event with the query event data watcherEmitter.emit('kyselyQuery', event); }, }), ], }) export class KyselyModule {}Integrate with
main.ts: In yourmain.tsfile, importcreateKyselyHandlerfrom@lensjs/watchersand pass it to thequeryWatcherconfiguration within thelensfunction. You can also configure optional settings likelogQueryErrorsToConsole.tsimport { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module.js'; import { lens } from '@lensjs/nestjs'; import { FastifyAdapter, NestFastifyApplication, } from '@nestjs/platform-fastify'; import { createKyselyHandler } from '@lensjs/watchers'; async function bootstrap() { const app = await NestFactory.create<NestFastifyApplication>( AppModule, new FastifyAdapter(), ); await lens({ adapter: 'fastify', app, cacheWatcherEnabled: true, queryWatcher: { enabled: true, handler: createKyselyHandler({ provider: 'sqlite', logQueryErrorsToConsole: true, // Optional: Set to true to log query errors to the console }), }, }); await app.listen(process.env.PORT ?? 3000); } bootstrap();
With these configurations, your NestJS application is now set up to use the Lens Query Watcher for Sequelize, Prisma, or Kysely, providing enhanced visibility into your database operations.
4. Custom Handlers
If your ORM or database client is not supported by the built-in handlers, you can create your own custom handler to integrate with Lens.
Handler Essentials
A custom handler must adhere to the QueryWatcherHandler interface and perform the following:
- Return a
QueryWatcherHandlerfunction. - Call the
onQuerycallback function whenever a query is captured, providing the necessary query details. - Optionally utilize
lensUtilsfor common tasks:interpolateQuery(sql, params): Injects parameters into a SQL query string.formatSqlQuery(query): Formats a SQL query for better readability.nowISO(): Generates a timestamp.
Example: Custom Handler for a Generic ORM
This example demonstrates how to create a custom handler for a generic ORM within a NestJS application. We'll create a dedicated service to manage the custom handler logic.
Create a Custom Query Emitter Service:
First, create a service that will be responsible for emitting custom query events. This service will expose a method to register a listener for your ORM's query events and then emit them in a format compatible with Lens.
ts// src/custom-query-emitter.service.ts import { Injectable } from '@nestjs/common'; import Emittery from 'emittery'; // You'll need to install this package: npm install emittery // Create a global or module-scoped event emitter for your custom ORM queries export const myOrmEventEmitter = new Emittery(); @Injectable() export class CustomQueryEmitterService { // This method would be called by your ORM's logging mechanism emitCustomQuery(query: string, params: any[], duration: number) { myOrmEventEmitter.emit('customOrmQuery', { query, params, duration }); } }Configure Your ORM's Logging:
Integrate your ORM's logging mechanism to call
emitCustomQueryfrom theCustomQueryEmitterService. The exact implementation will depend on your ORM. For example, if your ORM has aloggingcallback:ts// In your ORM configuration (e.g., a database module) import { CustomQueryEmitterService } from './custom-query-emitter.service'; // ... ORM setup ... const customQueryEmitterService = new CustomQueryEmitterService(); // Or inject it if in a NestJS module yourOrmInstance.configure({ logging: (query: string, params: any[], duration: number) => { customQueryEmitterService.emitCustomQuery(query, params, duration); }, });Create the Custom Query Watcher Handler:
Now, create the actual
QueryWatcherHandlerfunction that listens to the events emitted byCustomQueryEmitterServiceand formats them for Lens.ts// src/custom-query.handler.ts import { type QueryWatcherHandler } from '@lensjs/watchers'; import { lensUtils } from '@lensjs/core'; import { nowISO } from '@lensjs/date'; import { myOrmEventEmitter } from './custom-query-emitter.service'; // Import your custom event emitter export function createCustomQueryHandler(): QueryWatcherHandler { return async ({ onQuery }) => { const databaseType = 'your-orm-type'; // e.g., 'mongodb', 'cassandra' myOrmEventEmitter.on('customOrmQuery', (payload: { query: string; params: any[]; duration: number }) => { onQuery({ query: lensUtils.formatSqlQuery( lensUtils.interpolateQuery( payload.query, payload.params, ), databaseType, ), duration: `${payload.duration} ms`, type: databaseType, createdAt: nowISO(), }); }); }; }Integrate into
main.ts:Finally, import your
createCustomQueryHandlerfunction and pass it to thequeryWatcherconfiguration in yourmain.tsfile.ts// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module.js'; import { lens } from '@lensjs/nestjs'; import { createCustomQueryHandler } from './custom-query.handler.js'; // Adjust path as needed async function bootstrap() { const app = await NestFactory.create(AppModule); // Ensure CustomQueryEmitterService is provided in AppModule or a related module // const customQueryEmitterService = app.get(CustomQueryEmitterService); // (If your ORM logging needs the service instance directly) await lens({ app, queryWatcher: { enabled: true, handler: createCustomQueryHandler(), }, }); await app.listen(process.env.PORT ?? 3000); } bootstrap();
This setup allows you to capture and display queries from any ORM or database client in the Lens UI by creating a custom handler that translates its logging output into the Lens-compatible format.