Stellen Sie sich vor, Sie sind der Kapitän eines Schiffes auf einer langen Reise. Sie würden niemals ohne Kompass, Seekarten und Instrumente zur Überwachung des Wetters und der Schiffssysteme ablegen. Genauso braucht jede Anwendung “Instrumente” - Logging und Monitoring sind die Augen und Ohren Ihrer Anwendung. Sie erzählen Ihnen, was gerade passiert, warnen Sie vor Problemen und helfen Ihnen zu verstehen, wie Ihre Anwendung sich verhält.
Logging ist wie das Führen eines detaillierten Logbuchs - es dokumentiert alle wichtigen Ereignisse, Entscheidungen und Probleme, die auftreten. Monitoring ist wie ein Wachposten, der ständig überwacht und Alarm schlägt, wenn etwas nicht stimmt. Zusammen geben sie Ihnen die Kontrolle und das Verständnis, das Sie brauchen, um Ihre Anwendung erfolgreich zu betreiben.
Denken Sie an einen Arzt, der eine Diagnose stellt. Ohne Symptome, Vital-Parameter und Testergebnisse wäre das unmöglich. Genauso ist es mit Software - ohne Logs und Monitoring-Daten können wir keine fundierten Entscheidungen treffen. Wir wissen nicht, ob unsere Anwendung gesund ist, wo Probleme auftreten oder wie wir sie verbessern können.
Gutes Logging und Monitoring helfen uns bei verschiedenen kritischen Aufgaben:
Debugging und Troubleshooting: Wenn etwas schief geht, sind Logs unsere Zeitmaschine - sie zeigen uns genau, was passiert ist und in welcher Reihenfolge.
Performance-Optimierung: Monitoring-Daten zeigen uns Engpässe und Performance-Probleme, bevor sie zu echten Problemen werden.
Security: Logs können verdächtige Aktivitäten und Sicherheitsvorfälle aufdecken.
Business Intelligence: Logs enthalten wertvolle Informationen über Nutzerverhalten und Anwendungsnutzung.
Compliance: Viele Branchen erfordern detaillierte Audit-Logs für rechtliche Compliance.
// Ein praktisches Beispiel: Was würde ohne Logging passieren?
@Injectable()
export class PaymentService {
async processPayment(amount: number, cardToken: string): Promise<PaymentResult> {
// Ohne Logging: Ein "schwarzes Loch"
// - War die Zahlung erfolgreich?
// - Gab es Fehler bei der Kreditkarten-Validierung?
// - Wie lange hat die Verarbeitung gedauert?
// - Welche Zahlungen wurden versucht?
// - Bei Problemen: Wo fangen wir mit der Suche an?
return this.externalPaymentProvider.charge(amount, cardToken);
}
}
// Mit richtigem Logging: Vollständige Transparenz
@Injectable()
export class PaymentServiceWithLogging {
constructor(private logger: LoggerService) {}
async processPayment(amount: number, cardToken: string, userId: string): Promise<PaymentResult> {
const startTime = Date.now();
const paymentId = this.generatePaymentId();
this.logger.log({
message: 'Payment processing started',
paymentId,
userId,
amount,
// Niemals vollständige Kreditkartendaten loggen!
cardTokenPreview: cardToken.substring(0, 8) + '...',
timestamp: new Date().toISOString(),
});
try {
// Validierung loggen
this.logger.debug({
message: 'Validating payment parameters',
paymentId,
validations: {
amountValid: amount > 0,
cardTokenValid: cardToken.length > 0,
userIdValid: !!userId,
},
});
const result = await this.externalPaymentProvider.charge(amount, cardToken);
const duration = Date.now() - startTime;
if (result.success) {
this.logger.log({
message: 'Payment processed successfully',
paymentId,
userId,
amount,
transactionId: result.transactionId,
duration,
status: 'success',
});
} else {
this.logger.warn({
message: 'Payment processing failed',
paymentId,
userId,
amount,
errorCode: result.errorCode,
errorMessage: result.errorMessage,
duration,
status: 'failed',
});
}
return result;
} catch (error) {
const duration = Date.now() - startTime;
this.logger.error({
message: 'Payment processing error',
paymentId,
userId,
amount,
error: error.message,
errorStack: error.stack,
duration,
status: 'error',
});
throw error;
}
}
private generatePaymentId(): string {
return `pay_${Date.now()}_${Math.random().toString(36).substring(2)}`;
}
}NestJS kommt mit einem eingebauten Logger, der wie ein zuverlässiger Kompass alle Grundfunktionen bietet, die Sie für den Anfang brauchen. Er ist einfach zu verwenden, gut integriert und für viele Anwendungen völlig ausreichend.
Der NestJS Logger ist wie ein intelligenter Assistent, der automatisch wichtige Informationen über Ihre Anwendung sammelt. Er weiß, wann die Anwendung startet, welche Module geladen werden, wann HTTP-Requests ankommen und vieles mehr.
// Der einfachste Weg, den Built-in Logger zu verwenden
import { Logger } from '@nestjs/common';
@Injectable()
export class UserService {
// Erstelle einen Logger für diese Klasse
private readonly logger = new Logger(UserService.name);
async createUser(userData: CreateUserDto): Promise<User> {
// Informatives Log - für allgemeine Informationen
this.logger.log(`Creating new user with email: ${userData.email}`);
try {
// Debug-Information - nur in Development sichtbar
this.logger.debug(`User creation data: ${JSON.stringify(userData)}`);
const user = await this.userRepository.save(userData);
// Erfolg loggen
this.logger.log(`User created successfully with ID: ${user.id}`);
return user;
} catch (error) {
// Fehler loggen - sehr wichtig für Debugging
this.logger.error(
`Failed to create user with email ${userData.email}: ${error.message}`,
error.stack, // Stack Trace für detailliertes Debugging
);
throw error;
}
}
async findUserById(id: string): Promise<User> {
this.logger.debug(`Looking up user with ID: ${id}`);
const user = await this.userRepository.findOne({ where: { id } });
if (!user) {
// Warnung - etwas Unerwartetes, aber nicht kritisch
this.logger.warn(`User not found with ID: ${id}`);
throw new NotFoundException('User not found');
}
this.logger.debug(`User found: ${user.email}`);
return user;
}
}Log Levels sind wie die Lautstärke-Regler an einem Mischpult. Sie bestimmen, welche Nachrichten “laut genug” sind, um gehört zu werden. In der Entwicklung wollen wir alles hören (auch leise Debug-Nachrichten), aber in der Produktion interessieren uns nur die wichtigen Dinge.
// Die verschiedenen Log Levels im Detail
@Injectable()
export class OrderService {
private readonly logger = new Logger(OrderService.name);
async processOrder(orderData: CreateOrderDto): Promise<Order> {
const orderId = this.generateOrderId();
// VERBOSE (Level 0) - Sehr detaillierte Informationen
// Normalerweise nur für intensive Debugging
this.logger.verbose(`Starting order processing with data: ${JSON.stringify(orderData)}`);
// DEBUG (Level 1) - Entwicklungs-Informationen
// Hilfreich für Entwickler, aber nicht für Produktion
this.logger.debug(`Generated order ID: ${orderId}`);
this.logger.debug(`Order contains ${orderData.items.length} items`);
try {
// LOG (Level 2) - Allgemeine Informationen
// Wichtige Geschäftsereignisse, die wir verfolgen wollen
this.logger.log(`Processing order ${orderId} for user ${orderData.userId}`);
// Geschäftslogik...
const order = await this.createOrder(orderData);
await this.validateInventory(order);
await this.processPayment(order);
// Erfolgreiche Verarbeitung
this.logger.log(`Order ${orderId} processed successfully. Total: $${order.total}`);
return order;
} catch (error) {
if (error instanceof PaymentFailedException) {
// WARN (Level 3) - Probleme, die Aufmerksamkeit brauchen
// Nicht kritisch für das System, aber wichtig für das Geschäft
this.logger.warn(`Payment failed for order ${orderId}: ${error.message}`);
} else if (error instanceof InventoryException) {
this.logger.warn(`Inventory issue for order ${orderId}: ${error.message}`);
} else {
// ERROR (Level 4) - Schwerwiegende Probleme
// Diese brauchen sofortige Aufmerksamkeit
this.logger.error(
`Critical error processing order ${orderId}: ${error.message}`,
error.stack,
);
}
throw error;
}
}
private async processPayment(order: Order): Promise<void> {
this.logger.debug(`Starting payment processing for order ${order.id}`);
try {
const paymentResult = await this.paymentService.charge(order.total, order.paymentMethod);
if (paymentResult.success) {
this.logger.log(`Payment successful for order ${order.id}, transaction: ${paymentResult.transactionId}`);
} else {
this.logger.warn(`Payment declined for order ${order.id}: ${paymentResult.declineReason}`);
throw new PaymentFailedException(paymentResult.declineReason);
}
} catch (error) {
this.logger.error(
`Payment processing error for order ${order.id}: ${error.message}`,
error.stack,
);
throw error;
}
}
}Wie ein Fernglas, das Sie für verschiedene Zwecke anders einstellen, sollte Ihr Logger je nach Umgebung unterschiedlich konfiguriert sein.
// logger.config.ts - Umgebungsspezifische Logger-Konfiguration
export const getLoggerConfig = () => {
const environment = process.env.NODE_ENV || 'development';
const baseConfig = {
timestamp: true,
context: true,
};
switch (environment) {
case 'development':
return {
...baseConfig,
// In Development wollen wir alles sehen
logLevels: ['error', 'warn', 'log', 'debug', 'verbose'],
prettyPrint: true, // Schöne Formatierung für bessere Lesbarkeit
colorize: true, // Farbige Ausgabe
};
case 'test':
return {
...baseConfig,
// In Tests nur Fehler, um die Ausgabe sauber zu halten
logLevels: ['error'],
prettyPrint: false,
colorize: false,
};
case 'staging':
return {
...baseConfig,
// Staging ähnlich wie Produktion, aber etwas mehr Details
logLevels: ['error', 'warn', 'log'],
prettyPrint: false,
colorize: false,
outputFormat: 'json', // Strukturierte Logs für Log-Management-Tools
};
case 'production':
return {
...baseConfig,
// Produktion: Nur wichtige Nachrichten
logLevels: ['error', 'warn'],
prettyPrint: false,
colorize: false,
outputFormat: 'json',
// In Produktion könnten wir auch externes Logging verwenden
useExternalLogger: true,
};
default:
return baseConfig;
}
};
// Anwendung der Konfiguration
// main.ts
async function bootstrap() {
const loggerConfig = getLoggerConfig();
const app = await NestFactory.create(AppModule, {
logger: loggerConfig.logLevels,
});
// Weitere App-Konfiguration...
await app.listen(3000);
}Manchmal reicht der Standard-Logger nicht aus - wie ein maßgeschneiderter Anzug passt ein Custom Logger perfekt zu Ihren spezifischen Bedürfnissen. Sie können entscheiden, wie Logs formatiert werden, wo sie gespeichert werden und welche zusätzlichen Informationen sie enthalten.
// custom-logger.service.ts - Ein vollständig anpassbarer Logger
import { LoggerService, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as winston from 'winston';
import * as DailyRotateFile from 'winston-daily-rotate-file';
@Injectable()
export class CustomLoggerService implements LoggerService {
private winston: winston.Logger;
private context: string;
constructor(private configService: ConfigService) {
this.createWinstonLogger();
}
private createWinstonLogger(): void {
// Erstelle verschiedene "Transports" - Ziele für die Logs
const transports: winston.transport[] = [];
// Console Transport - für Development
if (this.configService.get('NODE_ENV') !== 'production') {
transports.push(
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.colorize(),
winston.format.printf(({ timestamp, level, message, context, ...meta }) => {
return `${timestamp} [${context}] ${level}: ${message} ${
Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''
}`;
}),
),
}),
);
}
// File Transport - für alle Umgebungen
transports.push(
new DailyRotateFile({
filename: 'logs/application-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true, // Komprimiere alte Logs
maxSize: '20m', // Maximale Dateigröße
maxFiles: '14d', // Behalte Logs für 14 Tage
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(), // JSON für bessere Parsbarkeit
),
}),
);
// Error File Transport - separate Datei für Fehler
transports.push(
new DailyRotateFile({
filename: 'logs/error-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '30d', // Fehler länger behalten
level: 'error', // Nur Fehler in diese Datei
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
}),
);
// HTTP Transport - für externe Log-Services (optional)
if (this.configService.get('LOG_EXTERNAL_URL')) {
transports.push(
new winston.transports.Http({
host: this.configService.get('LOG_EXTERNAL_HOST'),
port: this.configService.get('LOG_EXTERNAL_PORT'),
path: this.configService.get('LOG_EXTERNAL_PATH'),
}),
);
}
this.winston = winston.createLogger({
level: this.configService.get('LOG_LEVEL', 'info'),
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json(),
),
defaultMeta: {
service: this.configService.get('APP_NAME', 'nestjs-app'),
version: this.configService.get('APP_VERSION', '1.0.0'),
environment: this.configService.get('NODE_ENV', 'development'),
},
transports,
});
}
// Implementation der LoggerService Interface
log(message: any, context?: string): void {
this.winston.info(message, { context: context || this.context });
}
error(message: any, stack?: string, context?: string): void {
this.winston.error(message, {
context: context || this.context,
stack,
});
}
warn(message: any, context?: string): void {
this.winston.warn(message, { context: context || this.context });
}
debug(message: any, context?: string): void {
this.winston.debug(message, { context: context || this.context });
}
verbose(message: any, context?: string): void {
this.winston.verbose(message, { context: context || this.context });
}
// Erweiterte Methoden für strukturiertes Logging
logWithMetadata(
level: string,
message: string,
metadata: Record<string, any>,
context?: string,
): void {
this.winston.log(level, message, {
context: context || this.context,
...metadata,
});
}
// Spezielle Methoden für häufige Use Cases
logUserAction(
userId: string,
action: string,
resource?: string,
metadata?: Record<string, any>,
): void {
this.winston.info('User action performed', {
context: 'USER_ACTION',
userId,
action,
resource,
timestamp: new Date().toISOString(),
...metadata,
});
}
logPerformance(
operation: string,
duration: number,
metadata?: Record<string, any>,
): void {
this.winston.info('Performance metric', {
context: 'PERFORMANCE',
operation,
duration,
timestamp: new Date().toISOString(),
...metadata,
});
}
logSecurityEvent(
eventType: string,
severity: 'low' | 'medium' | 'high' | 'critical',
details: Record<string, any>,
): void {
this.winston.warn('Security event detected', {
context: 'SECURITY',
eventType,
severity,
timestamp: new Date().toISOString(),
...details,
});
}
// Context-Management
setContext(context: string): void {
this.context = context;
}
// Logger für spezifische Module erstellen
createModuleLogger(moduleName: string): CustomLoggerService {
const moduleLogger = new CustomLoggerService(this.configService);
moduleLogger.setContext(moduleName);
return moduleLogger;
}
}Structured Logging ist wie der Unterschied zwischen einem unorganisierten Notizbuch und einer gut sortierten Datenbank. Statt nur Text zu loggen, strukturieren wir unsere Logs so, dass sie leicht durchsuchbar und analysierbar sind.
// structured-logging.service.ts - Strukturierte Logs für bessere Analyse
@Injectable()
export class StructuredLoggingService {
constructor(private logger: CustomLoggerService) {}
// E-Commerce-spezifische Log-Strukturen
logOrderEvent(event: OrderLogEvent): void {
this.logger.logWithMetadata(
'info',
`Order event: ${event.eventType}`,
{
orderId: event.orderId,
userId: event.userId,
eventType: event.eventType,
orderValue: event.orderValue,
currency: event.currency,
paymentMethod: event.paymentMethod,
shippingAddress: event.shippingAddress?.city, // Keine vollständige Adresse loggen
itemCount: event.itemCount,
timestamp: new Date().toISOString(),
// Zusätzliche Metadaten für Analyse
metadata: {
sessionId: event.sessionId,
userAgent: event.userAgent,
ipAddress: this.hashIpAddress(event.ipAddress), // IP-Hash für Privacy
referrer: event.referrer,
},
},
'ORDER_EVENTS',
);
}
// API-Request/Response Logging
logApiRequest(request: ApiRequestLog): void {
this.logger.logWithMetadata(
'info',
`API Request: ${request.method} ${request.path}`,
{
requestId: request.requestId,
method: request.method,
path: request.path,
statusCode: request.statusCode,
duration: request.duration,
userAgent: request.userAgent,
userId: request.userId,
// Request-spezifische Daten
queryParams: this.sanitizeParams(request.queryParams),
bodySize: request.bodySize,
responseSize: request.responseSize,
// Performance-Metriken
performanceMetrics: {
dbQueryCount: request.dbQueryCount,
dbQueryTime: request.dbQueryTime,
cacheHits: request.cacheHits,
cacheMisses: request.cacheMisses,
},
timestamp: new Date().toISOString(),
},
'API_REQUESTS',
);
}
// Error Logging mit Kontext
logError(error: Error, context: ErrorContext): void {
this.logger.logWithMetadata(
'error',
error.message,
{
errorName: error.name,
errorMessage: error.message,
errorStack: error.stack,
// Kontext-Informationen
userId: context.userId,
requestId: context.requestId,
operation: context.operation,
// System-Informationen
nodeVersion: process.version,
platform: process.platform,
memoryUsage: process.memoryUsage(),
// Business-Kontext
businessContext: context.businessContext,
timestamp: new Date().toISOString(),
},
'ERRORS',
);
}
// Business Metrics Logging
logBusinessMetric(metric: BusinessMetric): void {
this.logger.logWithMetadata(
'info',
`Business metric: ${metric.name}`,
{
metricName: metric.name,
metricValue: metric.value,
metricUnit: metric.unit,
metricType: metric.type, // 'counter', 'gauge', 'histogram'
// Dimensionen für Gruppierung
dimensions: metric.dimensions,
// Zeitfenster-Informationen
timeWindow: metric.timeWindow,
aggregationType: metric.aggregationType,
timestamp: new Date().toISOString(),
},
'BUSINESS_METRICS',
);
}
// Security Event Logging
logSecurityEvent(securityEvent: SecurityEvent): void {
this.logger.logWithMetadata(
'warn',
`Security event: ${securityEvent.eventType}`,
{
eventType: securityEvent.eventType,
severity: securityEvent.severity,
userId: securityEvent.userId,
ipAddress: this.hashIpAddress(securityEvent.ipAddress),
userAgent: securityEvent.userAgent,
// Event-spezifische Details
attemptedAction: securityEvent.attemptedAction,
resource: securityEvent.resource,
outcome: securityEvent.outcome,
// Zusätzliche Sicherheitsinformationen
geolocation: securityEvent.geolocation,
deviceFingerprint: securityEvent.deviceFingerprint,
timestamp: new Date().toISOString(),
},
'SECURITY_EVENTS',
);
}
// Hilfsmethoden für Datenschutz und Sicherheit
private sanitizeParams(params: any): any {
const sensitiveKeys = ['password', 'token', 'secret', 'key', 'auth'];
const sanitized = { ...params };
Object.keys(sanitized).forEach(key => {
if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
sanitized[key] = '[REDACTED]';
}
});
return sanitized;
}
private hashIpAddress(ipAddress: string): string {
// Hashe IP-Adressen für Privacy, aber behalte Konsistenz für Analyse
const crypto = require('crypto');
return crypto.createHash('sha256').update(ipAddress).digest('hex').substring(0, 16);
}
}
// Typ-Definitionen für strukturierte Logs
interface OrderLogEvent {
orderId: string;
userId: string;
eventType: 'created' | 'updated' | 'cancelled' | 'shipped' | 'delivered';
orderValue: number;
currency: string;
paymentMethod: string;
shippingAddress?: { city: string; country: string };
itemCount: number;
sessionId?: string;
userAgent?: string;
ipAddress?: string;
referrer?: string;
}
interface ApiRequestLog {
requestId: string;
method: string;
path: string;
statusCode: number;
duration: number;
userAgent?: string;
userId?: string;
queryParams?: Record<string, any>;
bodySize: number;
responseSize: number;
dbQueryCount: number;
dbQueryTime: number;
cacheHits: number;
cacheMisses: number;
}
interface ErrorContext {
userId?: string;
requestId?: string;
operation: string;
businessContext?: Record<string, any>;
}
interface BusinessMetric {
name: string;
value: number;
unit: string;
type: 'counter' | 'gauge' | 'histogram';
dimensions: Record<string, string>;
timeWindow?: string;
aggregationType?: string;
}
interface SecurityEvent {
eventType: string;
severity: 'low' | 'medium' | 'high' | 'critical';
userId?: string;
ipAddress: string;
userAgent: string;
attemptedAction: string;
resource: string;
outcome: 'success' | 'failure' | 'blocked';
geolocation?: string;
deviceFingerprint?: string;
}Request/Response Logging ist wie eine Verkehrsüberwachung für Ihre API. Es verfolgt jeden Request, der in Ihre Anwendung hineinkommt und jeden Response, der hinausgeht. Das hilft beim Debugging, Performance-Monitoring und der Analyse von Nutzungsmustern.
// request-logging.interceptor.ts - Überwacht jeden HTTP-Request
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class RequestLoggingInterceptor implements NestInterceptor {
constructor(private structuredLogger: StructuredLoggingService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const httpContext = context.switchToHttp();
const request = httpContext.getRequest<Request>();
const response = httpContext.getResponse<Response>();
// Generiere eine eindeutige Request-ID für Tracking
const requestId = uuidv4();
request['requestId'] = requestId;
response.setHeader('X-Request-ID', requestId);
const startTime = Date.now();
const startCpuUsage = process.cpuUsage();
const startMemory = process.memoryUsage();
// Log Request Start
this.logRequestStart(request, requestId);
return next.handle().pipe(
tap((responseData) => {
// Log Success Response
this.logRequestSuccess(
request,
response,
responseData,
requestId,
startTime,
startCpuUsage,
startMemory,
);
}),
catchError((error) => {
// Log Error Response
this.logRequestError(
request,
response,
error,
requestId,
startTime,
startCpuUsage,
startMemory,
);
throw error; // Re-throw den Error
}),
);
}
private logRequestStart(request: Request, requestId: string): void {
this.structuredLogger.logWithMetadata(
'info',
`Request started: ${request.method} ${request.path}`,
{
requestId,
method: request.method,
path: request.path,
query: this.sanitizeQuery(request.query),
headers: this.sanitizeHeaders(request.headers),
userAgent: request.headers['user-agent'],
ip: this.getClientIp(request),
timestamp: new Date().toISOString(),
},
'REQUEST_START',
);
}
private logRequestSuccess(
request: Request,
response: Response,
responseData: any,
requestId: string,
startTime: number,
startCpuUsage: NodeJS.CpuUsage,
startMemory: NodeJS.MemoryUsage,
): void {
const duration = Date.now() - startTime;
const cpuUsage = process.cpuUsage(startCpuUsage);
const endMemory = process.memoryUsage();
this.structuredLogger.logApiRequest({
requestId,
method: request.method,
path: request.path,
statusCode: response.statusCode,
duration,
userAgent: request.headers['user-agent'],
userId: request['user']?.id,
queryParams: request.query,
bodySize: this.getRequestBodySize(request),
responseSize: this.calculateResponseSize(responseData),
// Performance-Metriken (diese würden normalerweise von anderen Services gesammelt)
dbQueryCount: request['dbQueryCount'] || 0,
dbQueryTime: request['dbQueryTime'] || 0,
cacheHits: request['cacheHits'] || 0,
cacheMisses: request['cacheMisses'] || 0,
});
// Zusätzliches Performance-Log für langsame Requests
if (duration > 1000) { // Langsamer als 1 Sekunde
this.structuredLogger.logWithMetadata(
'warn',
`Slow request detected: ${request.method} ${request.path}`,
{
requestId,
duration,
cpuUsage: {
user: cpuUsage.user / 1000, // Konvertiere zu Millisekunden
system: cpuUsage.system / 1000,
},
memoryDelta: {
heapUsed: endMemory.heapUsed - startMemory.heapUsed,
heapTotal: endMemory.heapTotal - startMemory.heapTotal,
},
timestamp: new Date().toISOString(),
},
'SLOW_REQUEST',
);
}
}
private logRequestError(
request: Request,
response: Response,
error: any,
requestId: string,
startTime: number,
startCpuUsage: NodeJS.CpuUsage,
startMemory: NodeJS.MemoryUsage,
): void {
const duration = Date.now() - startTime;
this.structuredLogger.logError(error, {
userId: request['user']?.id,
requestId,
operation: `${request.method} ${request.path}`,
businessContext: {
httpMethod: request.method,
path: request.path,
query: request.query,
statusCode: response.statusCode,
duration,
userAgent: request.headers['user-agent'],
ip: this.getClientIp(request),
},
});
}
// Hilfsmethoden für Datenbereinigung und -berechnung
private sanitizeQuery(query: any): any {
const sensitiveKeys = ['password', 'token', 'key', 'secret'];
const sanitized = { ...query };
Object.keys(sanitized).forEach(key => {
if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
sanitized[key] = '[REDACTED]';
}
});
return sanitized;
}
private sanitizeHeaders(headers: any): any {
const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key'];
const sanitized = { ...headers };
Object.keys(sanitized).forEach(key => {
if (sensitiveHeaders.includes(key.toLowerCase())) {
sanitized[key] = '[REDACTED]';
}
});
return sanitized;
}
private getClientIp(request: Request): string {
return (
request.headers['x-forwarded-for'] as string ||
request.headers['x-real-ip'] as string ||
request.connection.remoteAddress ||
request.ip ||
'unknown'
);
}
private getRequestBodySize(request: Request): number {
const contentLength = request.headers['content-length'];
return contentLength ? parseInt(contentLength, 10) : 0;
}
private calculateResponseSize(responseData: any): number {
if (!responseData) return 0;
try {
return JSON.stringify(responseData).length;
} catch {
return 0;
}
}
}Database Query Logging hilft dabei zu verstehen, wie Ihre Anwendung mit der Datenbank interagiert. Es kann Performance-Probleme aufdecken, ineffiziente Queries identifizieren und bei der Optimierung helfen.
// database-logging.interceptor.ts - Überwacht Datenbankzugriffe
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class DatabaseLoggingInterceptor implements NestInterceptor {
constructor(private structuredLogger: StructuredLoggingService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
// Initialisiere DB-Metriken für diesen Request
request['dbQueryCount'] = 0;
request['dbQueryTime'] = 0;
request['dbQueries'] = [];
return next.handle().pipe(
tap(() => {
// Log DB-Statistiken am Ende des Requests
this.logDatabaseStatistics(request);
}),
);
}
private logDatabaseStatistics(request: any): void {
const { dbQueryCount, dbQueryTime, dbQueries } = request;
if (dbQueryCount > 0) {
this.structuredLogger.logWithMetadata(
'info',
`Database statistics for request`,
{
requestId: request.requestId,
queryCount: dbQueryCount,
totalQueryTime: dbQueryTime,
averageQueryTime: dbQueryTime / dbQueryCount,
slowQueries: dbQueries.filter(q => q.duration > 100), // Queries > 100ms
timestamp: new Date().toISOString(),
},
'DATABASE_STATS',
);
}
// Warne vor zu vielen Queries (mögliches N+1 Problem)
if (dbQueryCount > 10) {
this.structuredLogger.logWithMetadata(
'warn',
`Excessive database queries detected`,
{
requestId: request.requestId,
queryCount: dbQueryCount,
path: `${request.method} ${request.path}`,
possibleN1Problem: true,
timestamp: new Date().toISOString(),
},
'DATABASE_WARNING',
);
}
}
}
// database-logger.service.ts - Service für detailliertes DB-Logging
@Injectable()
export class DatabaseLogger {
constructor(private structuredLogger: StructuredLoggingService) {}
logQuery(queryInfo: DatabaseQueryInfo): void {
const {
query,
parameters,
duration,
rowCount,
queryType,
tableName,
requestId,
} = queryInfo;
// Standard Query-Log
this.structuredLogger.logWithMetadata(
'debug',
`Database query executed`,
{
requestId,
queryType,
tableName,
duration,
rowCount,
query: this.sanitizeQuery(query),
parameters: this.sanitizeParameters(parameters),
timestamp: new Date().toISOString(),
},
'DATABASE_QUERY',
);
// Warne vor langsamen Queries
if (duration > 1000) { // > 1 Sekunde
this.structuredLogger.logWithMetadata(
'warn',
`Slow database query detected`,
{
requestId,
queryType,
tableName,
duration,
query: this.sanitizeQuery(query),
timestamp: new Date().toISOString(),
},
'SLOW_QUERY',
);
}
// Aktualisiere Request-Metriken (falls verfügbar)
if (requestId && global['currentRequests']?.[requestId]) {
const request = global['currentRequests'][requestId];
request.dbQueryCount = (request.dbQueryCount || 0) + 1;
request.dbQueryTime = (request.dbQueryTime || 0) + duration;
request.dbQueries = request.dbQueries || [];
request.dbQueries.push({
query: this.sanitizeQuery(query),
duration,
tableName,
queryType,
});
}
}
private sanitizeQuery(query: string): string {
// Entferne oder verkürze sehr lange Queries
if (query.length > 500) {
return query.substring(0, 500) + '... [TRUNCATED]';
}
// Entferne potentiell sensible Daten aus WHERE-Klauseln
return query.replace(
/(\bWHERE\s+\w+\s*=\s*)(['"][^'"]*['"])/gi,
'$1[REDACTED]'
);
}
private sanitizeParameters(parameters: any[]): any[] {
if (!parameters) return [];
return parameters.map(param => {
if (typeof param === 'string' && param.length > 100) {
return '[LONG_STRING]';
}
return param;
});
}
}
interface DatabaseQueryInfo {
query: string;
parameters?: any[];
duration: number;
rowCount?: number;
queryType: 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE';
tableName?: string;
requestId?: string;
}Performance Monitoring ist wie regelmäßige Gesundheitschecks für Ihre Anwendung. Es überwacht wichtige Vital-Parameter und warnt Sie, bevor kleine Probleme zu großen werden.
// performance-monitor.service.ts - Überwacht Application Performance
@Injectable()
export class PerformanceMonitorService {
private metrics = new Map<string, PerformanceMetric[]>();
private readonly maxMetricsPerType = 1000; // Verhindere Memory Leaks
constructor(private structuredLogger: StructuredLoggingService) {
// Starte regelmäßige System-Metriken-Sammlung
this.startSystemMetricsCollection();
}
// Messe die Ausführungszeit einer Operation
async measureOperation<T>(
operationName: string,
operation: () => Promise<T>,
context?: Record<string, any>,
): Promise<T> {
const startTime = Date.now();
const startCpuUsage = process.cpuUsage();
const startMemory = process.memoryUsage();
try {
const result = await operation();
// Messe Erfolgs-Metriken
this.recordOperationMetrics(
operationName,
Date.now() - startTime,
true,
startCpuUsage,
startMemory,
context,
);
return result;
} catch (error) {
// Messe Fehler-Metriken
this.recordOperationMetrics(
operationName,
Date.now() - startTime,
false,
startCpuUsage,
startMemory,
{ ...context, error: error.message },
);
throw error;
}
}
private recordOperationMetrics(
operationName: string,
duration: number,
success: boolean,
startCpuUsage: NodeJS.CpuUsage,
startMemory: NodeJS.MemoryUsage,
context?: Record<string, any>,
): void {
const endCpuUsage = process.cpuUsage(startCpuUsage);
const endMemory = process.memoryUsage();
const metric: PerformanceMetric = {
operationName,
duration,
success,
timestamp: Date.now(),
cpuUsage: {
user: endCpuUsage.user / 1000, // Konvertiere zu Millisekunden
system: endCpuUsage.system / 1000,
},
memoryDelta: {
heapUsed: endMemory.heapUsed - startMemory.heapUsed,
heapTotal: endMemory.heapTotal - startMemory.heapTotal,
external: endMemory.external - startMemory.external,
},
context,
};
// Speichere Metrik
this.addMetric(operationName, metric);
// Log die Metrik
this.structuredLogger.logPerformance(
operationName,
duration,
{
success,
cpuUsage: metric.cpuUsage,
memoryDelta: metric.memoryDelta,
context,
},
);
// Warne vor Performance-Problemen
this.checkPerformanceThresholds(metric);
}
private addMetric(operationName: string, metric: PerformanceMetric): void {
if (!this.metrics.has(operationName)) {
this.metrics.set(operationName, []);
}
const operationMetrics = this.metrics.get(operationName);
operationMetrics.push(metric);
// Begrenze die Anzahl gespeicherter Metriken
if (operationMetrics.length > this.maxMetricsPerType) {
operationMetrics.shift(); // Entferne die älteste Metrik
}
}
private checkPerformanceThresholds(metric: PerformanceMetric): void {
const { operationName, duration, memoryDelta } = metric;
// Warne vor langsamen Operationen
if (duration > 5000) { // > 5 Sekunden
this.structuredLogger.logWithMetadata(
'warn',
`Very slow operation detected: ${operationName}`,
{
operationName,
duration,
threshold: 5000,
timestamp: new Date().toISOString(),
},
'PERFORMANCE_WARNING',
);
}
// Warne vor hohem Speicherverbrauch
if (memoryDelta.heapUsed > 50 * 1024 * 1024) { // > 50MB
this.structuredLogger.logWithMetadata(
'warn',
`High memory usage detected: ${operationName}`,
{
operationName,
memoryUsage: memoryDelta.heapUsed,
threshold: 50 * 1024 * 1024,
timestamp: new Date().toISOString(),
},
'MEMORY_WARNING',
);
}
}
// Berechne Statistiken für eine Operation
getOperationStatistics(operationName: string): OperationStatistics {
const metrics = this.metrics.get(operationName) || [];
if (metrics.length === 0) {
return null;
}
const durations = metrics.map(m => m.duration);
const successCount = metrics.filter(m => m.success).length;
return {
operationName,
totalCalls: metrics.length,
successRate: (successCount / metrics.length) * 100,
averageDuration: durations.reduce((a, b) => a + b, 0) / durations.length,
minDuration: Math.min(...durations),
maxDuration: Math.max(...durations),
p95Duration: this.calculatePercentile(durations, 95),
p99Duration: this.calculatePercentile(durations, 99),
lastCall: new Date(Math.max(...metrics.map(m => m.timestamp))),
};
}
// Sammle System-weite Metriken
private startSystemMetricsCollection(): void {
setInterval(() => {
this.collectSystemMetrics();
}, 30000); // Alle 30 Sekunden
}
private collectSystemMetrics(): void {
const memoryUsage = process.memoryUsage();
const cpuUsage = process.cpuUsage();
this.structuredLogger.logWithMetadata(
'info',
'System metrics collected',
{
memory: {
heapUsed: memoryUsage.heapUsed,
heapTotal: memoryUsage.heapTotal,
external: memoryUsage.external,
rss: memoryUsage.rss,
},
cpu: {
user: cpuUsage.user / 1000,
system: cpuUsage.system / 1000,
},
uptime: process.uptime(),
activeHandles: (process as any)._getActiveHandles?.()?.length || 0,
activeRequests: (process as any)._getActiveRequests?.()?.length || 0,
timestamp: new Date().toISOString(),
},
'SYSTEM_METRICS',
);
// Warne vor kritischen System-Zuständen
this.checkSystemHealth(memoryUsage, cpuUsage);
}
private checkSystemHealth(
memoryUsage: NodeJS.MemoryUsage,
cpuUsage: NodeJS.CpuUsage,
): void {
// Memory-Warnung (> 80% des verfügbaren Heaps)
const heapUsagePercent = (memoryUsage.heapUsed / memoryUsage.heapTotal) * 100;
if (heapUsagePercent > 80) {
this.structuredLogger.logWithMetadata(
'warn',
'High memory usage detected',
{
heapUsagePercent,
heapUsed: memoryUsage.heapUsed,
heapTotal: memoryUsage.heapTotal,
timestamp: new Date().toISOString(),
},
'SYSTEM_WARNING',
);
}
// CPU-Warnung ist schwieriger zu messen, da cpuUsage kumulativ ist
// In einer echten Anwendung würden Sie hier ein CPU-Monitoring-Tool verwenden
}
private calculatePercentile(values: number[], percentile: number): number {
const sorted = values.slice().sort((a, b) => a - b);
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
return sorted[index];
}
// API für externe Performance-Tests
getAllStatistics(): Record<string, OperationStatistics> {
const allStats: Record<string, OperationStatistics> = {};
for (const operationName of this.metrics.keys()) {
allStats[operationName] = this.getOperationStatistics(operationName);
}
return allStats;
}
// Cleanup für Memory-Management
clearOldMetrics(olderThanMs: number = 24 * 60 * 60 * 1000): void {
const cutoffTime = Date.now() - olderThanMs;
for (const [operationName, metrics] of this.metrics.entries()) {
const filteredMetrics = metrics.filter(m => m.timestamp > cutoffTime);
this.metrics.set(operationName, filteredMetrics);
}
}
}
// Typ-Definitionen für Performance-Monitoring
interface PerformanceMetric {
operationName: string;
duration: number;
success: boolean;
timestamp: number;
cpuUsage: {
user: number;
system: number;
};
memoryDelta: {
heapUsed: number;
heapTotal: number;
external: number;
};
context?: Record<string, any>;
}
interface OperationStatistics {
operationName: string;
totalCalls: number;
successRate: number;
averageDuration: number;
minDuration: number;
maxDuration: number;
p95Duration: number;
p99Duration: number;
lastCall: Date;
}Health Checks sind wie der Puls Ihrer Anwendung - sie geben schnell Auskunft darüber, ob alles in Ordnung ist oder ob Aufmerksamkeit benötigt wird. Sie sind besonders wichtig in Container-Umgebungen und Load Balancern, die wissen müssen, ob sie Traffic an eine Instanz weiterleiten können.
// health-check.service.ts - Umfassende Gesundheitsüberwachung
import { Injectable } from '@nestjs/common';
import {
HealthCheckService,
HealthCheck,
HealthCheckResult,
HealthIndicator,
HealthIndicatorResult,
HealthCheckError,
} from '@nestjs/terminus';
@Injectable()
export class ApplicationHealthService {
constructor(
private health: HealthCheckService,
private databaseHealthIndicator: DatabaseHealthIndicator,
private redisHealthIndicator: RedisHealthIndicator,
private externalApiHealthIndicator: ExternalApiHealthIndicator,
private diskHealthIndicator: DiskHealthIndicator,
private memoryHealthIndicator: MemoryHealthIndicator,
private structuredLogger: StructuredLoggingService,
) {}
@HealthCheck()
async checkLiveness(): Promise<HealthCheckResult> {
// Liveness Check - ist die Anwendung am Leben?
// Sollte nur die allergrundlegendsten Checks enthalten
return this.health.check([
() => this.basicApplicationCheck(),
]);
}
@HealthCheck()
async checkReadiness(): Promise<HealthCheckResult> {
// Readiness Check - ist die Anwendung bereit, Traffic zu empfangen?
// Umfasst alle kritischen Dependencies
return this.health.check([
() => this.basicApplicationCheck(),
() => this.databaseHealthIndicator.isHealthy('database'),
() => this.redisHealthIndicator.isHealthy('redis'),
() => this.memoryHealthIndicator.checkHeap('memory'),
() => this.diskHealthIndicator.checkStorage('disk'),
]);
}
@HealthCheck()
async checkComplete(): Promise<HealthCheckResult> {
// Vollständiger Health Check - umfasst auch nicht-kritische Services
const result = await this.health.check([
() => this.basicApplicationCheck(),
() => this.databaseHealthIndicator.isHealthy('database'),
() => this.redisHealthIndicator.isHealthy('redis'),
() => this.memoryHealthIndicator.checkHeap('memory'),
() => this.diskHealthIndicator.checkStorage('disk'),
() => this.externalApiHealthIndicator.checkPaymentService('payment-api'),
() => this.externalApiHealthIndicator.checkEmailService('email-api'),
]);
// Log den Health Check
this.logHealthCheckResult(result);
return result;
}
private async basicApplicationCheck(): Promise<HealthIndicatorResult> {
const isHealthy = true; // Basis-Check - App läuft
const result = {
'basic-check': {
status: isHealthy ? 'up' : 'down',
uptime: process.uptime(),
timestamp: new Date().toISOString(),
version: process.env.APP_VERSION || '1.0.0',
nodeVersion: process.version,
platform: process.platform,
},
};
if (isHealthy) {
return result;
} else {
throw new HealthCheckError('Basic application check failed', result);
}
}
private logHealthCheckResult(result: HealthCheckResult): void {
const status = result.status;
const checks = result.info || result.error;
this.structuredLogger.logWithMetadata(
status === 'ok' ? 'info' : 'error',
`Health check completed with status: ${status}`,
{
status,
checks,
timestamp: new Date().toISOString(),
},
'HEALTH_CHECK',
);
// Warne vor fehlgeschlagenen Checks
if (status !== 'ok' && result.error) {
Object.entries(result.error).forEach(([checkName, checkResult]) => {
this.structuredLogger.logWithMetadata(
'warn',
`Health check failed: ${checkName}`,
{
checkName,
checkResult,
timestamp: new Date().toISOString(),
},
'HEALTH_CHECK_FAILURE',
);
});
}
}
}
// Custom Health Indicators für verschiedene Services
@Injectable()
export class DatabaseHealthIndicator extends HealthIndicator {
constructor(
@InjectDataSource() private dataSource: DataSource,
private structuredLogger: StructuredLoggingService,
) {
super();
}
async isHealthy(key: string): Promise<HealthIndicatorResult> {
const startTime = Date.now();
try {
// Führe eine einfache Query aus
await this.dataSource.query('SELECT 1');
const responseTime = Date.now() - startTime;
const result = this.getStatus(key, true, {
responseTime,
connection: 'active',
timestamp: new Date().toISOString(),
});
// Warne vor langsamen DB-Responses
if (responseTime > 1000) {
this.structuredLogger.logWithMetadata(
'warn',
'Database health check slow',
{ responseTime, threshold: 1000 },
'DATABASE_HEALTH',
);
}
return result;
} catch (error) {
const responseTime = Date.now() - startTime;
this.structuredLogger.logWithMetadata(
'error',
'Database health check failed',
{
error: error.message,
responseTime,
timestamp: new Date().toISOString(),
},
'DATABASE_HEALTH',
);
throw new HealthCheckError(
'Database check failed',
this.getStatus(key, false, {
error: error.message,
responseTime,
timestamp: new Date().toISOString(),
}),
);
}
}
}
@Injectable()
export class RedisHealthIndicator extends HealthIndicator {
constructor(
@Inject('REDIS_CLIENT') private redisClient: any,
private structuredLogger: StructuredLoggingService,
) {
super();
}
async isHealthy(key: string): Promise<HealthIndicatorResult> {
const startTime = Date.now();
try {
// Ping Redis
await this.redisClient.ping();
const responseTime = Date.now() - startTime;
return this.getStatus(key, true, {
responseTime,
connection: 'active',
timestamp: new Date().toISOString(),
});
} catch (error) {
const responseTime = Date.now() - startTime;
this.structuredLogger.logWithMetadata(
'error',
'Redis health check failed',
{
error: error.message,
responseTime,
timestamp: new Date().toISOString(),
},
'REDIS_HEALTH',
);
throw new HealthCheckError(
'Redis check failed',
this.getStatus(key, false, {
error: error.message,
responseTime,
timestamp: new Date().toISOString(),
}),
);
}
}
}
@Injectable()
export class MemoryHealthIndicator extends HealthIndicator {
constructor(private structuredLogger: StructuredLoggingService) {
super();
}
checkHeap(key: string): HealthIndicatorResult {
const memoryUsage = process.memoryUsage();
const heapUsed = memoryUsage.heapUsed;
const heapTotal = memoryUsage.heapTotal;
const heapUsagePercent = (heapUsed / heapTotal) * 100;
// Kritischer Speicherverbrauch bei > 90%
const isHealthy = heapUsagePercent < 90;
const result = this.getStatus(key, isHealthy, {
heapUsed,
heapTotal,
heapUsagePercent: Math.round(heapUsagePercent * 100) / 100,
rss: memoryUsage.rss,
external: memoryUsage.external,
timestamp: new Date().toISOString(),
});
if (!isHealthy) {
this.structuredLogger.logWithMetadata(
'error',
'Memory health check failed - high heap usage',
{
heapUsagePercent,
heapUsed,
heapTotal,
threshold: 90,
timestamp: new Date().toISOString(),
},
'MEMORY_HEALTH',
);
throw new HealthCheckError('Memory usage too high', result);
}
// Warne bei > 80% Speicherverbrauch
if (heapUsagePercent > 80) {
this.structuredLogger.logWithMetadata(
'warn',
'Memory health check warning - high heap usage',
{
heapUsagePercent,
heapUsed,
heapTotal,
threshold: 80,
timestamp: new Date().toISOString(),
},
'MEMORY_HEALTH',
);
}
return result;
}
}
@Injectable()
export class ExternalApiHealthIndicator extends HealthIndicator {
constructor(
private httpService: HttpService,
private structuredLogger: StructuredLoggingService,
) {
super();
}
async checkPaymentService(key: string): Promise<HealthIndicatorResult> {
return this.checkExternalService(
key,
'https://api.payment-provider.com/health',
'Payment Service',
5000, // 5 Sekunden Timeout
);
}
async checkEmailService(key: string): Promise<HealthIndicatorResult> {
return this.checkExternalService(
key,
'https://api.sendgrid.com/v3/user/profile',
'Email Service',
3000, // 3 Sekunden Timeout
);
}
private async checkExternalService(
key: string,
url: string,
serviceName: string,
timeoutMs: number,
): Promise<HealthIndicatorResult> {
const startTime = Date.now();
try {
const response = await this.httpService.get(url, {
timeout: timeoutMs,
headers: {
'User-Agent': 'HealthCheck/1.0',
},
}).toPromise();
const responseTime = Date.now() - startTime;
const isHealthy = response.status >= 200 && response.status < 300;
if (isHealthy) {
return this.getStatus(key, true, {
service: serviceName,
statusCode: response.status,
responseTime,
url,
timestamp: new Date().toISOString(),
});
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
const responseTime = Date.now() - startTime;
this.structuredLogger.logWithMetadata(
'warn',
`External service health check failed: ${serviceName}`,
{
service: serviceName,
url,
error: error.message,
responseTime,
timestamp: new Date().toISOString(),
},
'EXTERNAL_SERVICE_HEALTH',
);
// Externe Services sind oft nicht kritisch für die Anwendung
// Daher loggen wir als Warning statt Error
return this.getStatus(key, false, {
service: serviceName,
error: error.message,
responseTime,
url,
timestamp: new Date().toISOString(),
});
}
}
}Logging und Monitoring sind wie das Nervensystem Ihrer Anwendung - sie geben Ihnen die Augen und Ohren, die Sie brauchen, um zu verstehen, was in Ihrer Anwendung passiert. Eine gut durchdachte Logging- und Monitoring-Strategie macht den Unterschied zwischen einer Anwendung, die Sie blind betreiben, und einer, die Sie vollständig unter Kontrolle haben.
Denken Sie daran: Gutes Logging und Monitoring sind eine Investition in die Zukunft. Die Zeit, die Sie heute in die Einrichtung verwenden, spart Ihnen später unzählige Stunden beim Debugging und hilft Ihnen, Probleme zu lösen, bevor Ihre Benutzer sie bemerken.