@starbemtech/star-node-stack-helper
Version:
A helper library for Node.js applications that provides utilities for AWS Secrets Manager integration and Elasticsearch/OpenSearch logging with enterprise-grade features.
262 lines (228 loc) • 7.2 kB
text/typescript
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { Observable } from 'rxjs'
import { tap, catchError } from 'rxjs/operators'
import { ElasticLogger } from '../../logger'
import { LogTransaction } from '../../logger/types'
import {
TRANSACTION_LOG_METADATA_KEY,
TransactionLogMetadata,
} from '../decorators/transaction-log.decorator'
export interface AutoTransactionLogInterceptorOptions {
elasticLogger: ElasticLogger | null
defaultMicroservice?: string
skipTransactionLogging?: boolean
}
()
export class AutoTransactionLogInterceptor implements NestInterceptor {
private readonly logger = new Logger(AutoTransactionLogInterceptor.name)
constructor(
private readonly reflector: Reflector,
private readonly options: AutoTransactionLogInterceptorOptions
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
if (!this.options.elasticLogger || this.options.skipTransactionLogging) {
return next.handle()
}
// Get metadata from decorator
const metadata = this.reflector.getAllAndOverride<TransactionLogMetadata>(
TRANSACTION_LOG_METADATA_KEY,
[context.getHandler(), context.getClass()]
)
// If no metadata, don't do transaction logging
if (!metadata) {
return next.handle()
}
const startTime = Date.now()
const request = context.switchToHttp().getRequest()
const response = context.switchToHttp().getResponse()
// Generate or get transaction ID
const transactionId =
request.headers['x-transaction-id'] ||
request.headers['x-request-id'] ||
`tx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
// Add transaction ID to request
request.transactionId = transactionId
// Set response headers
response.setHeader('X-Transaction-ID', transactionId)
// Use microservice from decorator or default
const microservice =
metadata.microservice ||
this.options.defaultMicroservice ||
'unknown-service'
// Create request metadata
const requestMeta = {
method: request.method,
path: request.url,
...(request.ip ? { ip: request.ip } : {}),
...(request.headers['user-agent']
? { userAgent: request.headers['user-agent'] }
: {}),
}
return next.handle().pipe(
tap((data) => {
const duration = Date.now() - startTime
const status = response.statusCode >= 400 ? 'fail' : 'success'
// Create transaction context
const transactionContext = this.buildTransactionContext(
request,
context,
data,
metadata
)
// Create transaction data
const transactionData: LogTransaction = {
name: metadata.operation,
microservice,
transactionId,
operation: metadata.operation,
status,
duration,
context: transactionContext,
requestMeta,
responseMeta: {
statusCode: response.statusCode,
responseSize: this.calculateResponseSize(data),
},
}
// Log transaction asynchronously
this.logTransactionAsync(transactionData)
}),
catchError((error) => {
const duration = Date.now() - startTime
const status = 'fail'
// Create transaction context with error
const transactionContext = this.buildTransactionContext(
request,
context,
null,
metadata,
error
)
// Create transaction data with error
const transactionData: LogTransaction = {
name: metadata.operation,
microservice,
transactionId,
operation: metadata.operation,
status,
duration,
context: transactionContext,
requestMeta,
responseMeta: {
statusCode: error.status || 500,
responseSize: 0,
},
error: {
message: error.message || 'Unknown error',
code: error.code,
stack: error.stack,
},
}
// Log transaction with error asynchronously
this.logTransactionAsync(transactionData)
throw error
})
)
}
private buildTransactionContext(
request: any,
_executionContext: ExecutionContext,
_responseData?: any,
metadata?: TransactionLogMetadata,
error?: any
): Record<string, unknown> {
const context: Record<string, unknown> = {}
// Add custom context from decorator
if (metadata?.customContext) {
Object.assign(context, metadata.customContext)
}
// Add URL parameters
if (request.params) {
Object.assign(context, request.params)
}
// Add query parameters (filtering sensitive data)
if (request.query) {
const filteredQuery = this.sanitizeObject(request.query, [
'token',
'password',
'newPassword',
'secret',
])
Object.assign(context, filteredQuery)
}
// Add request body (filtering sensitive data)
if (request.body && typeof request.body === 'object') {
const filteredBody = this.sanitizeObject(request.body, [
'password',
'newPassword',
'token',
'secret',
])
Object.assign(context, filteredBody)
}
// Add relevant headers
const relevantHeaders = [
'x-user-id',
'x-appointment-id',
'x-platform',
'authorization',
'content-type',
'accept',
]
relevantHeaders.forEach((header) => {
const value = request.headers[header]
if (value) {
context[header] = header === 'authorization' ? '[REDACTED]' : value
}
})
// Add user information if available
if (request.user) {
context['userId'] = request.user.id || request.user.userId
context['userRole'] = request.user.role
}
// Add error information if present
if (error) {
context['errorType'] = error.constructor.name
context['errorMessage'] = error.message
}
return context
}
private sanitizeObject(obj: any, sensitiveFields: string[]): any {
const sanitized = { ...obj }
sensitiveFields.forEach((field) => {
if (sanitized[field]) {
sanitized[field] = '[REDACTED]'
}
})
return sanitized
}
private calculateResponseSize(data: any): number {
if (!data) return 0
try {
return JSON.stringify(data).length
} catch {
return 0
}
}
private logTransactionAsync(transactionData: LogTransaction): void {
setImmediate(async () => {
try {
await this.options.elasticLogger!.logTransaction(transactionData)
} catch (error) {
this.logger.error('❌ Error registering transaction log:', {
transactionId: transactionData.transactionId,
microservice: transactionData.microservice,
operation: transactionData.operation,
error: error instanceof Error ? error.message : 'Unknown error',
})
}
})
}
}