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', {
level: 'info',
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.
NESTLENS_BULL_QUEUES
Register Bull/BullMQ queues for job tracking:
import { NESTLENS_BULL_QUEUES } from 'nestlens';
import { Queue } from 'bull';
import { getQueueToken } from '@nestjs/bull';
@Module({
providers: [
{
provide: NESTLENS_BULL_QUEUES,
useFactory: (
emailQueue: Queue,
smsQueue: Queue,
) => [
{ queue: emailQueue, name: 'email' },
{ queue: smsQueue, name: 'sms' },
],
inject: [
getQueueToken('email'),
getQueueToken('sms'),
],
},
],
})
export class QueueModule {}
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'] = {
level: 'info',
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
- Check Watcher Enabled: Verify the watcher for your entry type is enabled
- Verify Payload: Ensure payload matches entry type structure
- Check Filters: Make sure filters aren't excluding your entries
- Console Logs: Add logging to confirm
collect()is called
Performance Issues
- Use Buffered Collection: Prefer
collect()overcollectImmediate() - Limit Payload Size: Keep payloads small
- Async Processing: Ensure tracking is non-blocking
- Batch Operations: Use
filterBatch()for bulk processing
Next Steps
- Learn about TypeORM Integration
- Explore Prisma Integration
- Configure Advanced Filtering
- Extend Storage Backend