Skip to main content

Custom Integrations

NestLens provides flexible APIs for creating custom integrations, extending existing watchers, and tracking custom events in your application.

Manual Tracking API​

Track custom events using the CollectorService directly.

Basic Usage​

import { Injectable } from '@nestjs/common';
import { CollectorService } from 'nestlens';

@Injectable()
export class PaymentService {
constructor(private collector: CollectorService) {}

async processPayment(orderId: number, amount: number) {
const startTime = Date.now();

try {
const result = await this.paymentGateway.charge(amount);

// Track successful payment
await this.collector.collect('http-client', {
method: 'POST',
url: 'https://api.payment-gateway.com/charge',
statusCode: 200,
duration: Date.now() - startTime,
requestBody: { amount },
responseBody: result,
});

return result;
} catch (error) {
// Track failed payment
await this.collector.collect('http-client', {
method: 'POST',
url: 'https://api.payment-gateway.com/charge',
duration: Date.now() - startTime,
error: error.message,
});

throw error;
}
}
}

Collect Methods​

collect() - Buffered Collection​

For normal tracking with automatic buffering:

async collect<T extends EntryType>(
type: T,
payload: Extract<Entry, { type: T }>['payload'],
requestId?: string,
): Promise<void>

Example:

await this.collector.collect('log', {
// 'debug' | 'log' | 'warn' | 'error' | 'verbose'. There is no 'info'.
level: 'log',
message: 'User logged in',
context: 'AuthService',
});

collectImmediate() - Immediate Save​

For critical events that need immediate storage:

async collectImmediate<T extends EntryType>(
type: T,
payload: Extract<Entry, { type: T }>['payload'],
requestId?: string,
): Promise<Entry | null>

Example:

const entry = await this.collector.collectImmediate('exception', {
name: 'PaymentError',
message: 'Payment gateway timeout',
stack: error.stack,
});

Request ID Correlation​

Associate custom events with HTTP requests using the request ID header:

import { REQUEST_ID_HEADER } from 'nestlens';
import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';

@Injectable({ scope: Scope.REQUEST })
export class CustomService {
constructor(
private collector: CollectorService,
@Inject(REQUEST) private request: Request,
) {}

async customOperation() {
// Get the request ID from headers
const requestId = this.request.headers[REQUEST_ID_HEADER] as string;

// This will be linked to the current HTTP request
await this.collector.collect('log', {
level: 'log',
message: 'Custom operation',
context: 'CustomService',
}, requestId);
}
}

Custom Watcher Tokens​

Create custom watchers using provider tokens.

Registering Bull/BullMQ Queues​

:::note Queues are registered by calling There is no provider token for queues; call the JobWatcher setup methods from an OnModuleInit hook. :::

import { Injectable, OnModuleInit } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull';
import { JobWatcher } from 'nestlens';

@Injectable()
export class QueueRegistration implements OnModuleInit {
constructor(
@InjectQueue('email') private emailQueue: Queue,
@InjectQueue('sms') private smsQueue: Queue,
private jobWatcher: JobWatcher,
) {}

async onModuleInit() {
this.jobWatcher.setupQueue(this.emailQueue, 'email');
this.jobWatcher.setupQueue(this.smsQueue, 'sms');
}
}

For BullMQ, use await this.jobWatcher.setupBullMQQueue(queue, name) instead. See Bull / BullMQ Integration.

NESTLENS_REDIS_CLIENT​

Register Redis client for command tracking:

import { NESTLENS_REDIS_CLIENT } from 'nestlens';
import Redis from 'ioredis';

@Module({
providers: [
{
provide: NESTLENS_REDIS_CLIENT,
useFactory: () => new Redis({
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT),
}),
},
],
})
export class RedisModule {}

NESTLENS_MODEL_SUBSCRIBER​

Register TypeORM entity subscriber for model tracking:

import { NESTLENS_MODEL_SUBSCRIBER } from 'nestlens';
import { EntitySubscriberInterface } from 'typeorm';

@Injectable()
@EventSubscriber()
export class CustomEntitySubscriber implements EntitySubscriberInterface {
// Implement subscriber methods
}

@Module({
providers: [
CustomEntitySubscriber,
{
provide: NESTLENS_MODEL_SUBSCRIBER,
useExisting: CustomEntitySubscriber,
},
],
})
export class DatabaseModule {}

Creating Custom Watchers​

Build your own watchers for specialized tracking.

Step 1: Define Entry Type​

If using an existing entry type, skip this. Otherwise, consider using a flexible type like log or event.

Step 2: Create Watcher Service​

import { Injectable, Logger } from '@nestjs/common';
import { CollectorService } from 'nestlens';

@Injectable()
export class CustomWatcher {
private readonly logger = new Logger(CustomWatcher.name);

constructor(private collector: CollectorService) {}

async trackCustomEvent(data: any) {
try {
await this.collector.collect('event', {
name: 'custom-event',
payload: data,
listeners: [],
duration: 0,
});
} catch (error) {
this.logger.error(`Failed to track custom event: ${error}`);
}
}
}

Step 3: Use Interceptors or Decorators​

Create reusable tracking patterns:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class CustomTrackingInterceptor implements NestInterceptor {
constructor(private collector: CollectorService) {}

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const startTime = Date.now();
const handler = context.getHandler().name;

return next.handle().pipe(
tap({
next: (result) => {
this.collector.collect('log', {
level: 'log',
message: `Handler ${handler} executed successfully`,
metadata: {
duration: Date.now() - startTime,
result: typeof result,
},
});
},
error: (error) => {
this.collector.collect('exception', {
name: error.name,
message: error.message,
stack: error.stack,
context: handler,
});
},
}),
);
}
}

Extending Existing Watchers​

Customize built-in watchers for your needs.

Extending Request Watcher​

Add custom request tagging:

import { NestLensModule } from 'nestlens';

NestLensModule.forRoot({
watchers: {
request: {
enabled: true,
tags: async (req) => {
const tags: string[] = [];

// Tag by user role
if (req.user?.role === 'admin') {
tags.push('admin-request');
}

// Tag by endpoint
if (req.path.startsWith('/api/v2')) {
tags.push('api-v2');
}

// Tag by feature flag
if (req.headers['x-feature-flag']) {
tags.push(`feature:${req.headers['x-feature-flag']}`);
}

return tags;
},
},
},
})

Extending Exception Watcher​

Custom exception handling:

import { ExceptionWatcherConfig } from 'nestlens';

const exceptionConfig: ExceptionWatcherConfig = {
enabled: true,
ignoreExceptions: [
'NotFoundException',
'UnauthorizedException',
],
};

NestLensModule.forRoot({
watchers: {
exception: exceptionConfig,
},
})

Extending Log Watcher​

Custom log filtering:

NestLensModule.forRoot({
watchers: {
log: {
enabled: true,
minLevel: 'warn', // Only track warnings and errors
},
},
})

Advanced Patterns​

Conditional Tracking​

Track events only under certain conditions:

@Injectable()
export class ConditionalTrackingService {
constructor(private collector: CollectorService) {}

async trackIfEnabled(eventName: string, data: any) {
// Only track in development and staging
if (['development', 'staging'].includes(process.env.NODE_ENV)) {
await this.collector.collect('event', {
name: eventName,
payload: data,
listeners: [],
duration: 0,
});
}
}
}

Custom Entry Filtering​

Filter entries before collection:

NestLensModule.forRoot({
filter: async (entry) => {
// Don't track successful health check requests
if (entry.type === 'request' &&
entry.payload.path === '/health' &&
entry.payload.statusCode === 200) {
return false;
}

// Don't track debug logs in production
if (entry.type === 'log' &&
entry.payload.level === 'debug' &&
process.env.NODE_ENV === 'production') {
return false;
}

return true;
},
})

Batch Entry Processing​

Process multiple entries at once:

NestLensModule.forRoot({
filterBatch: async (entries) => {
// Remove duplicate queries
const uniqueQueries = new Map();

return entries.filter(entry => {
if (entry.type === 'query') {
const key = entry.payload.query;
if (uniqueQueries.has(key)) {
return false;
}
uniqueQueries.set(key, true);
}
return true;
});
},
})

Custom Tagging​

Auto-tag entries based on content:

import { TagService } from 'nestlens';

@Injectable()
export class CustomTaggingService {
constructor(private tagService: TagService) {}

async tagEntry(entryId: number, entry: Entry) {
const tags: string[] = [];

// Tag slow operations
if (entry.type === 'request' && entry.payload.duration > 1000) {
tags.push('slow-request');
}

// Tag by status code
if (entry.type === 'request') {
const status = entry.payload.statusCode;
if (status >= 400) {
tags.push(`status-${Math.floor(status / 100)}xx`);
}
}

// Tag database errors
if (entry.type === 'exception' &&
entry.payload.message.includes('database')) {
tags.push('database-error');
}

if (tags.length > 0) {
await this.tagService.addTags(entryId, tags);
}
}
}

Real-World Examples​

Third-Party API Tracking​

@Injectable()
export class ExternalApiService {
constructor(
private httpService: HttpService,
private collector: CollectorService,
) {}

async callExternalApi(endpoint: string, data: any) {
const startTime = Date.now();

try {
const response = await this.httpService
.post(`https://api.example.com${endpoint}`, data)
.toPromise();

// Track successful API call
await this.collector.collect('http-client', {
method: 'POST',
url: `https://api.example.com${endpoint}`,
requestBody: data,
statusCode: response.status,
responseBody: response.data,
duration: Date.now() - startTime,
});

return response.data;
} catch (error) {
// Track failed API call
await this.collector.collect('http-client', {
method: 'POST',
url: `https://api.example.com${endpoint}`,
requestBody: data,
duration: Date.now() - startTime,
error: error.message,
});

throw error;
}
}
}

File Upload Tracking​

@Injectable()
export class FileUploadService {
constructor(private collector: CollectorService) {}

async uploadFile(file: Express.Multer.File) {
const startTime = Date.now();

try {
const result = await this.s3.upload(file);

await this.collector.collect('batch', {
name: 'file-upload',
operation: 'upload',
totalItems: 1,
processedItems: 1,
failedItems: 0,
duration: Date.now() - startTime,
status: 'completed',
memory: file.size,
});

return result;
} catch (error) {
await this.collector.collect('batch', {
name: 'file-upload',
operation: 'upload',
totalItems: 1,
processedItems: 0,
failedItems: 1,
duration: Date.now() - startTime,
status: 'failed',
errors: [error.message],
});

throw error;
}
}
}

Custom Metrics Tracking​

@Injectable()
export class MetricsService {
constructor(private collector: CollectorService) {}

async trackBusinessMetric(name: string, value: number, metadata?: any) {
await this.collector.collect('log', {
level: 'log',
message: `Metric: ${name}`,
context: 'Metrics',
metadata: {
metricName: name,
metricValue: value,
timestamp: new Date(),
...metadata,
},
});
}

async trackUserAction(userId: number, action: string) {
await this.collector.collect('event', {
name: 'user-action',
payload: { userId, action },
listeners: [],
duration: 0,
});
}
}

Best Practices​

1. Use Type-Safe Entry Payloads​

Leverage TypeScript for compile-time safety:

import { LogEntry } from 'nestlens';

const logPayload: LogEntry['payload'] = {
// 'debug' | 'log' | 'warn' | 'error' | 'verbose' — this is what the type
// buys you: 'info' does not compile.
level: 'log',
message: 'Custom log',
context: 'MyService',
};

await this.collector.collect('log', logPayload);

2. Handle Errors Gracefully​

Don't let tracking break your app:

try {
await this.collector.collect('log', payload);
} catch (error) {
// Log but don't throw
console.error('Failed to track event:', error);
}

3. Avoid Blocking Operations​

Use buffered collection for performance:

// Good - buffered, non-blocking
await this.collector.collect('event', payload);

// Use sparingly - immediate, blocking
await this.collector.collectImmediate('exception', payload);

4. Include Request Context​

Link events to requests when possible:

await this.collector.collect('log', payload, this.requestId);

5. Document Custom Integrations​

Help your team understand custom tracking:

/**
* Tracks payment processing events
* @param orderId - Order identifier
* @param amount - Payment amount in cents
*/
async trackPayment(orderId: number, amount: number) {
// ...
}

Troubleshooting​

Custom Events Not Appearing​

  1. Check Watcher Enabled: Verify the watcher for your entry type is enabled
  2. Verify Payload: Ensure payload matches entry type structure
  3. Check Filters: Make sure filters aren't excluding your entries
  4. Console Logs: Add logging to confirm collect() is called

Performance Issues​

  1. Use Buffered Collection: Prefer collect() over collectImmediate()
  2. Limit Payload Size: Keep payloads small
  3. Async Processing: Ensure tracking is non-blocking
  4. Batch Operations: Use filterBatch() for bulk processing

Next Steps​