Stellen Sie sich vor, Sie sind ein Detektiv, der einen rätselhaften Fall lösen muss. Sie haben Hinweise, aber diese sind über verschiedene Schauplätze verstreut, manchmal widersprüchlich und oft unvollständig. Genau so fühlt sich Debugging in komplexen NestJS-Anwendungen an. Der Unterschied ist, dass Sie als Entwickler nicht nur der Detektiv sind, sondern auch derjenige, der die Werkzeuge und Strategien entwickeln kann, um solche Rätsel systematisch zu lösen.
Debugging und Troubleshooting sind Fähigkeiten, die über den reinen Code hinausgehen. Sie erfordern analytisches Denken, Geduld und vor allem die richtigen Werkzeuge und Techniken. In modernen NestJS-Anwendungen, die oft aus dutzenden von Modulen bestehen, mit externen APIs kommunizieren und in komplexen Infrastrukturen laufen, wird effektives Debugging zu einer kritischen Kompetenz.
Was macht Debugging in NestJS-Anwendungen besonders herausfordernd? Die Antwort liegt in der Architektur selbst. Dependency Injection, Decorators, Guards, Interceptors und Pipes schaffen zwar eine elegante und modulare Struktur, aber sie können auch dazu führen, dass der Ausführungsfluss nicht immer offensichtlich ist. Ein Request durchläuft möglicherweise mehrere Layer, bevor er die eigentliche Business Logic erreicht, und an jedem Punkt können Probleme auftreten.
Denken Sie an eine NestJS-Anwendung wie an ein Orchester. Wenn ein Ton falsch klingt, müssen Sie nicht nur herausfinden, welches Instrument das Problem verursacht, sondern auch verstehen, wie alle Instrumente zusammenarbeiten und sich gegenseitig beeinflussen. Diese ganzheitliche Sichtweise ist der Schlüssel zu erfolgreichem Debugging.
Die Grundlage jeder erfolgreichen Debugging-Session ist eine systematische Herangehensweise. Wie ein Wissenschaftler, der ein Experiment durchführt, sollten Sie Hypothesen aufstellen, diese testen und basierend auf den Ergebnissen Ihre nächsten Schritte planen. Diese methodische Herangehensweise verhindert, dass Sie in die Falle des “Trial-and-Error”-Debuggings tappen, bei dem Sie zufällige Änderungen vornehmen und hoffen, dass das Problem verschwindet.
Der erste Schritt einer effektiven Debugging-Strategie ist das Verstehen des Problemkontexts. Tritt das Problem nur unter bestimmten Bedingungen auf? Betrifft es bestimmte Benutzer oder Daten? Ist es ein neues Problem oder bestand es schon länger? Diese Fragen helfen Ihnen, den Suchbereich einzugrenzen und Ihre Debugging-Bemühungen zu fokussieren.
Eine bewährte Technik ist das “Divide and Conquer”-Prinzip. Anstatt zu versuchen, das gesamte System auf einmal zu verstehen, teilen Sie es in kleinere, handhabbare Teile auf. In NestJS bedeutet das oft, Module für Module zu isolieren und zu testen.
// debugging/src/utils/debug-logger.service.ts
// Ein spezialisierter Logger für Debugging-Zwecke
import { Injectable, Logger, LogLevel } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
interface DebugContext {
requestId?: string;
userId?: string;
module?: string;
method?: string;
metadata?: Record<string, any>;
}
@Injectable()
export class DebugLoggerService extends Logger {
private readonly isDebugMode: boolean;
private readonly debugModules: Set<string>;
constructor(private readonly configService: ConfigService) {
super('DebugLogger');
// Debug-Modus aus Umgebungsvariablen bestimmen
this.isDebugMode = this.configService.get<string>('NODE_ENV') === 'development' ||
this.configService.get<boolean>('DEBUG_MODE', false);
// Spezifische Module für Debugging aktivieren
const debugModulesStr = this.configService.get<string>('DEBUG_MODULES', '');
this.debugModules = new Set(debugModulesStr.split(',').filter(Boolean));
}
/**
* Erweiterte Debug-Logging-Methode mit Kontext
* Diese Methode hilft dabei, den Ausführungsfluss zu verfolgen
*/
debugWithContext(message: string, context: DebugContext, data?: any): void {
if (!this.shouldLog(context.module)) {
return;
}
const enrichedMessage = this.buildContextualMessage(message, context);
if (data) {
// Komplexe Objekte werden strukturiert ausgegeben für bessere Lesbarkeit
this.debug(enrichedMessage);
this.debug(`Data: ${JSON.stringify(data, null, 2)}`);
} else {
this.debug(enrichedMessage);
}
}
/**
* Verfolgt den Eintritt in eine Methode
* Nützlich für das Verstehen des Ausführungsflusses
*/
traceMethodEntry(className: string, methodName: string, params?: any[], context?: DebugContext): void {
if (!this.isDebugMode) return;
const baseMessage = `→ Entering ${className}.${methodName}()`;
const fullContext = { ...context, module: className, method: methodName };
if (params && params.length > 0) {
this.debugWithContext(baseMessage, fullContext, { parameters: params });
} else {
this.debugWithContext(baseMessage, fullContext);
}
}
/**
* Verfolgt den Austritt aus einer Methode
* Hilfreich für das Verstehen von Rückgabewerten und Ausführungszeit
*/
traceMethodExit(className: string, methodName: string, result?: any, executionTime?: number, context?: DebugContext): void {
if (!this.isDebugMode) return;
const baseMessage = `← Exiting ${className}.${methodName}()`;
const fullContext = { ...context, module: className, method: methodName };
const data: any = {};
if (result !== undefined) data.result = result;
if (executionTime !== undefined) data.executionTimeMs = executionTime;
this.debugWithContext(baseMessage, fullContext, Object.keys(data).length > 0 ? data : undefined);
}
/**
* Protokolliert Datenbankoperationen für Performance-Analyse
*/
traceQuery(query: string, parameters?: any[], executionTime?: number, context?: DebugContext): void {
if (!this.shouldLog('database')) return;
const message = `🗄️ Database Query`;
const fullContext = { ...context, module: 'database' };
this.debugWithContext(message, fullContext, {
query: query.replace(/\s+/g, ' ').trim(), // Normalisiere Whitespace
parameters,
executionTimeMs: executionTime,
});
}
/**
* Protokolliert HTTP-Requests zu externen Services
*/
traceHttpRequest(method: string, url: string, headers?: any, body?: any, context?: DebugContext): void {
if (!this.shouldLog('http')) return;
const message = `🌐 HTTP Request: ${method} ${url}`;
const fullContext = { ...context, module: 'http' };
this.debugWithContext(message, fullContext, {
method,
url,
headers: this.sanitizeHeaders(headers),
body: this.sanitizeBody(body),
});
}
/**
* Protokolliert Fehler mit zusätzlichem Kontext
* Essentiell für das Verstehen von Fehlerzuständen
*/
traceError(error: Error, context: DebugContext, additionalData?: any): void {
const message = `❌ Error in ${context.module || 'unknown'}.${context.method || 'unknown'}`;
this.error(this.buildContextualMessage(message, context));
this.error(`Error Type: ${error.name}`);
this.error(`Error Message: ${error.message}`);
if (error.stack) {
this.error(`Stack Trace:\n${error.stack}`);
}
if (additionalData) {
this.error(`Additional Data: ${JSON.stringify(additionalData, null, 2)}`);
}
}
private shouldLog(module?: string): boolean {
if (!this.isDebugMode) return false;
if (!module) return true;
if (this.debugModules.size === 0) return true;
return this.debugModules.has(module) || this.debugModules.has('*');
}
private buildContextualMessage(message: string, context: DebugContext): string {
const parts = [message];
if (context.requestId) parts.push(`[Req: ${context.requestId}]`);
if (context.userId) parts.push(`[User: ${context.userId}]`);
if (context.metadata) {
const metaStr = Object.entries(context.metadata)
.map(([key, value]) => `${key}=${value}`)
.join(', ');
parts.push(`[${metaStr}]`);
}
return parts.join(' ');
}
private sanitizeHeaders(headers: any): any {
if (!headers) return undefined;
const sanitized = { ...headers };
// Entferne sensible Header für Logging
delete sanitized.authorization;
delete sanitized.cookie;
delete sanitized['x-api-key'];
return sanitized;
}
private sanitizeBody(body: any): any {
if (!body) return undefined;
if (typeof body !== 'object') return body;
const sanitized = { ...body };
// Entferne sensible Felder
delete sanitized.password;
delete sanitized.token;
delete sanitized.secret;
return sanitized;
}
}Ein wichtiger Aspekt des Debuggings in NestJS ist das Verstehen der Decorator-Pipeline. Jeder Request durchläuft mehrere Stufen: Guards, Interceptors, Pipes und schließlich die eigentliche Route Handler. Ein Decorator, der speziell für das Debugging entwickelt wurde, kann Ihnen helfen, diesen Fluss zu visualisieren.
// debugging/src/decorators/debug-trace.decorator.ts
// Ein Decorator für automatisches Method-Tracing
import { SetMetadata } from '@nestjs/common';
export const DEBUG_TRACE_KEY = 'debug_trace';
/**
* Decorator für automatisches Tracing von Methodenaufrufen
* Verwenden Sie diesen Decorator auf Controller-Methoden oder Service-Methoden,
* um automatisch Entry/Exit-Logs zu erhalten
*/
export const DebugTrace = (options?: {
logParams?: boolean;
logResult?: boolean;
logExecutionTime?: boolean;
}) => {
const defaultOptions = {
logParams: true,
logResult: true,
logExecutionTime: true,
...options,
};
return SetMetadata(DEBUG_TRACE_KEY, defaultOptions);
};
// debugging/src/interceptors/debug-trace.interceptor.ts
// Der zugehörige Interceptor für den DebugTrace Decorator
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { DebugLoggerService } from '../utils/debug-logger.service';
import { DEBUG_TRACE_KEY } from '../decorators/debug-trace.decorator';
@Injectable()
export class DebugTraceInterceptor implements NestInterceptor {
constructor(
private readonly debugLogger: DebugLoggerService,
private readonly reflector: Reflector,
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// Prüfe, ob die Methode mit @DebugTrace markiert ist
const traceOptions = this.reflector.get<any>(DEBUG_TRACE_KEY, context.getHandler());
if (!traceOptions) {
return next.handle();
}
const className = context.getClass().name;
const methodName = context.getHandler().name;
const request = context.switchToHttp().getRequest();
// Extrahiere relevante Kontext-Informationen
const debugContext = {
requestId: request.requestId || request.headers['x-request-id'],
userId: request.user?.id,
module: className,
method: methodName,
};
const startTime = Date.now();
// Protokolliere Methoden-Eintritt
if (traceOptions.logParams) {
const params = this.extractMethodParameters(context);
this.debugLogger.traceMethodEntry(className, methodName, params, debugContext);
} else {
this.debugLogger.traceMethodEntry(className, methodName, undefined, debugContext);
}
return next.handle().pipe(
tap({
next: (result) => {
const executionTime = traceOptions.logExecutionTime ? Date.now() - startTime : undefined;
const resultToLog = traceOptions.logResult ? result : undefined;
this.debugLogger.traceMethodExit(className, methodName, resultToLog, executionTime, debugContext);
},
error: (error) => {
const executionTime = Date.now() - startTime;
this.debugLogger.traceError(error, debugContext, {
executionTimeMs: executionTime,
methodParameters: traceOptions.logParams ? this.extractMethodParameters(context) : undefined,
});
},
}),
);
}
private extractMethodParameters(context: ExecutionContext): any[] {
const request = context.switchToHttp().getRequest();
const params = [];
// Extrahiere verschiedene Arten von Parametern
if (request.params && Object.keys(request.params).length > 0) {
params.push({ type: 'params', data: request.params });
}
if (request.query && Object.keys(request.query).length > 0) {
params.push({ type: 'query', data: request.query });
}
if (request.body && Object.keys(request.body).length > 0) {
// Sensible Daten aus Body entfernen für Logging
const sanitizedBody = this.sanitizeObject(request.body);
params.push({ type: 'body', data: sanitizedBody });
}
return params;
}
private sanitizeObject(obj: any): any {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
const sanitized = { ...obj };
const sensitiveFields = ['password', 'token', 'secret', 'apiKey', 'authorization'];
for (const field of sensitiveFields) {
if (field in sanitized) {
sanitized[field] = '[REDACTED]';
}
}
return sanitized;
}
}Das Verstehen von NestJS Exception Flows ist ein weiterer kritischer Aspekt des Debuggings. Exceptions können an verschiedenen Punkten in der Request-Pipeline auftreten, und es ist wichtig zu verstehen, wie sie sich durch das System bewegen.
Performance-Probleme sind oft wie unsichtbare Krankheiten - die Symptome sind offensichtlich (langsame Response-Zeiten, hohe CPU-Auslastung), aber die Ursachen können tief im Code verborgen sein. Performance Profiling ist der Prozess, diese versteckten Engpässe aufzuspüren und zu verstehen, wo Ihre Anwendung Zeit und Ressourcen verbraucht.
Denken Sie an Performance Profiling wie an eine Gesundheitsuntersuchung für Ihre Anwendung. Genau wie ein Arzt verschiedene Tests durchführt, um herauszufinden, warum Sie sich unwohl fühlen, verwenden Sie verschiedene Profiling-Techniken, um herauszufinden, warum Ihre Anwendung langsam ist.
Der erste Schritt beim Performance Profiling ist das Sammeln von Baseline-Metriken. Ohne zu wissen, wie sich Ihre Anwendung unter normalen Bedingungen verhält, können Sie nicht erkennen, was abnormal ist.
// profiling/src/services/performance-profiler.service.ts
// Ein umfassendes Performance-Profiling-System
import { Injectable, Logger } from '@nestjs/common';
import { performance, PerformanceObserver } from 'perf_hooks';
import * as os from 'os';
import * as process from 'process';
interface PerformanceMetrics {
timestamp: Date;
cpuUsage: NodeJS.CpuUsage;
memoryUsage: NodeJS.MemoryUsage;
systemLoad: number[];
eventLoopDelay: number;
activeHandles: number;
activeRequests: number;
}
interface MethodPerformance {
methodName: string;
className: string;
executionTime: number;
memoryDelta: number;
cpuTime: number;
callCount: number;
lastCalled: Date;
}
@Injectable()
export class PerformanceProfilerService {
private readonly logger = new Logger(PerformanceProfilerService.name);
// Speichert Performance-Metriken für verschiedene Methoden
private readonly methodMetrics = new Map<string, MethodPerformance>();
// Sammelt System-Metriken über Zeit
private readonly systemMetrics: PerformanceMetrics[] = [];
private readonly maxMetricsHistory = 1000; // Speichere nur die letzten 1000 Einträge
// Performance Observer für automatische Messungen
private performanceObserver: PerformanceObserver;
constructor() {
this.initializePerformanceObserver();
this.startSystemMetricsCollection();
}
/**
* Startet die Profiling-Messung für eine spezifische Operation
* Verwenden Sie dies am Anfang einer Methode, die Sie profilen möchten
*/
startProfiling(className: string, methodName: string): string {
const profilingId = `${className}.${methodName}_${Date.now()}_${Math.random()}`;
// Markiere den Start-Zeitpunkt
performance.mark(`${profilingId}_start`);
return profilingId;
}
/**
* Beendet die Profiling-Messung und protokolliert die Ergebnisse
* Verwenden Sie dies am Ende der Methode, zusammen mit der startProfiling-ID
*/
endProfiling(profilingId: string, additionalData?: Record<string, any>): PerformanceMetrics {
const endMark = `${profilingId}_end`;
const measureName = `${profilingId}_duration`;
// Markiere den End-Zeitpunkt
performance.mark(endMark);
// Messe die Dauer zwischen Start und Ende
performance.measure(measureName, `${profilingId}_start`, endMark);
const measure = performance.getEntriesByName(measureName)[0];
const executionTime = measure.duration;
// Extrahiere Klassen- und Methodennamen aus der Profiling-ID
const [classAndMethod] = profilingId.split('_');
const [className, methodName] = classAndMethod.split('.');
// Aktualisiere Methoden-Metriken
this.updateMethodMetrics(className, methodName, executionTime);
// Sammle aktuelle System-Metriken
const currentMetrics = this.getCurrentSystemMetrics();
// Protokolliere Performance-Daten für spätere Analyse
this.logger.debug(`Performance: ${classAndMethod} executed in ${executionTime.toFixed(2)}ms`, {
className,
methodName,
executionTime,
systemMetrics: currentMetrics,
additionalData,
});
// Aufräumen: Entferne Performance-Marks
performance.clearMarks(`${profilingId}_start`);
performance.clearMarks(endMark);
performance.clearMeasures(measureName);
return currentMetrics;
}
/**
* Erstellt ein detailliertes Performance-Profil der Anwendung
* Nützlich für die Analyse von Performance-Trends und Engpässen
*/
getPerformanceProfile(): {
systemMetrics: PerformanceMetrics;
methodMetrics: MethodPerformance[];
topSlowMethods: MethodPerformance[];
memoryHotspots: MethodPerformance[];
recommendations: string[];
} {
const currentSystemMetrics = this.getCurrentSystemMetrics();
const allMethodMetrics = Array.from(this.methodMetrics.values());
// Identifiziere die langsamsten Methoden
const topSlowMethods = allMethodMetrics
.sort((a, b) => b.executionTime - a.executionTime)
.slice(0, 10);
// Identifiziere Memory-Hotspots
const memoryHotspots = allMethodMetrics
.filter(m => m.memoryDelta > 0)
.sort((a, b) => b.memoryDelta - a.memoryDelta)
.slice(0, 10);
// Generiere Performance-Empfehlungen
const recommendations = this.generatePerformanceRecommendations(currentSystemMetrics, allMethodMetrics);
return {
systemMetrics: currentSystemMetrics,
methodMetrics: allMethodMetrics,
topSlowMethods,
memoryHotspots,
recommendations,
};
}
/**
* Analysiert Performance-Trends über Zeit
* Hilfreich für das Erkennen von Performance-Degradationen
*/
getPerformanceTrends(timeWindowMinutes: number = 60): {
averageExecutionTime: number;
memoryTrend: 'increasing' | 'decreasing' | 'stable';
cpuTrend: 'increasing' | 'decreasing' | 'stable';
alertsGenerated: string[];
} {
const cutoffTime = new Date(Date.now() - timeWindowMinutes * 60 * 1000);
const recentMetrics = this.systemMetrics.filter(m => m.timestamp > cutoffTime);
if (recentMetrics.length < 2) {
return {
averageExecutionTime: 0,
memoryTrend: 'stable',
cpuTrend: 'stable',
alertsGenerated: ['Insufficient data for trend analysis'],
};
}
// Berechne Durchschnittswerte
const avgExecutionTime = this.calculateAverageExecutionTime(recentMetrics);
// Analysiere Trends
const memoryTrend = this.analyzeTrend(recentMetrics.map(m => m.memoryUsage.heapUsed));
const cpuTrend = this.analyzeTrend(recentMetrics.map(m => m.cpuUsage.user + m.cpuUsage.system));
// Generiere Alerts bei problematischen Trends
const alerts = this.generatePerformanceAlerts(recentMetrics);
return {
averageExecutionTime: avgExecutionTime,
memoryTrend,
cpuTrend,
alertsGenerated: alerts,
};
}
private initializePerformanceObserver(): void {
this.performanceObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
for (const entry of entries) {
// Protokolliere automatisch langsame Operationen
if (entry.duration > 100) { // Mehr als 100ms
this.logger.warn(`Slow operation detected: ${entry.name} took ${entry.duration.toFixed(2)}ms`);
}
}
});
// Überwache verschiedene Performance-Entry-Typen
this.performanceObserver.observe({ entryTypes: ['measure', 'function'] });
}
private startSystemMetricsCollection(): void {
// Sammle System-Metriken alle 30 Sekunden
setInterval(() => {
const metrics = this.getCurrentSystemMetrics();
this.systemMetrics.push(metrics);
// Halte nur die letzten N Einträge
if (this.systemMetrics.length > this.maxMetricsHistory) {
this.systemMetrics.shift();
}
// Protokolliere kritische System-Zustände
this.checkSystemHealth(metrics);
}, 30000);
}
private getCurrentSystemMetrics(): PerformanceMetrics {
return {
timestamp: new Date(),
cpuUsage: process.cpuUsage(),
memoryUsage: process.memoryUsage(),
systemLoad: os.loadavg(),
eventLoopDelay: this.measureEventLoopDelay(),
activeHandles: (process as any)._getActiveHandles().length,
activeRequests: (process as any)._getActiveRequests().length,
};
}
private measureEventLoopDelay(): number {
// Vereinfachte Event Loop Delay-Messung
// In einer produktiven Anwendung würden Sie ein spezialisiertes Paket verwenden
const start = process.hrtime.bigint();
setImmediate(() => {
const delay = Number(process.hrtime.bigint() - start) / 1000000; // Konvertiere zu Millisekunden
return delay;
});
return 0; // Placeholder für diese vereinfachte Implementierung
}
private updateMethodMetrics(className: string, methodName: string, executionTime: number): void {
const key = `${className}.${methodName}`;
const existing = this.methodMetrics.get(key);
if (existing) {
// Aktualisiere bestehende Metriken
existing.executionTime = (existing.executionTime + executionTime) / 2; // Gleitender Durchschnitt
existing.callCount += 1;
existing.lastCalled = new Date();
} else {
// Erstelle neue Metriken
this.methodMetrics.set(key, {
methodName,
className,
executionTime,
memoryDelta: 0, // Würde in einer vollständigen Implementierung berechnet
cpuTime: 0, // Würde in einer vollständigen Implementierung berechnet
callCount: 1,
lastCalled: new Date(),
});
}
}
private checkSystemHealth(metrics: PerformanceMetrics): void {
const memoryUsagePercent = (metrics.memoryUsage.heapUsed / metrics.memoryUsage.heapTotal) * 100;
const loadAverage = metrics.systemLoad[0]; // 1-Minuten-Load-Average
// Speicher-Warnung
if (memoryUsagePercent > 85) {
this.logger.warn(`High memory usage detected: ${memoryUsagePercent.toFixed(1)}%`);
}
// CPU-Load-Warnung
const cpuCount = os.cpus().length;
if (loadAverage > cpuCount * 0.8) {
this.logger.warn(`High CPU load detected: ${loadAverage.toFixed(2)} (${cpuCount} cores available)`);
}
// Event Loop-Blockierung-Warnung
if (metrics.eventLoopDelay > 50) {
this.logger.warn(`Event loop delay detected: ${metrics.eventLoopDelay.toFixed(2)}ms`);
}
}
private calculateAverageExecutionTime(metrics: PerformanceMetrics[]): number {
// Diese Implementierung ist vereinfacht
// In einer echten Anwendung würden Sie spezifische Method-Execution-Times verfolgen
return metrics.reduce((sum, m) => sum + m.eventLoopDelay, 0) / metrics.length;
}
private analyzeTrend(values: number[]): 'increasing' | 'decreasing' | 'stable' {
if (values.length < 3) return 'stable';
const first = values.slice(0, values.length / 3).reduce((a, b) => a + b) / (values.length / 3);
const last = values.slice(-values.length / 3).reduce((a, b) => a + b) / (values.length / 3);
const percentChange = ((last - first) / first) * 100;
if (percentChange > 10) return 'increasing';
if (percentChange < -10) return 'decreasing';
return 'stable';
}
private generatePerformanceRecommendations(
systemMetrics: PerformanceMetrics,
methodMetrics: MethodPerformance[]
): string[] {
const recommendations: string[] = [];
const memoryUsagePercent = (systemMetrics.memoryUsage.heapUsed / systemMetrics.memoryUsage.heapTotal) * 100;
if (memoryUsagePercent > 80) {
recommendations.push('Consider implementing memory caching strategies to reduce heap usage');
}
const slowMethods = methodMetrics.filter(m => m.executionTime > 100);
if (slowMethods.length > 0) {
recommendations.push(`Optimize ${slowMethods.length} methods with execution times > 100ms`);
}
if (systemMetrics.activeHandles > 100) {
recommendations.push('High number of active handles detected - check for potential resource leaks');
}
return recommendations;
}
private generatePerformanceAlerts(metrics: PerformanceMetrics[]): string[] {
const alerts: string[] = [];
const latestMetrics = metrics[metrics.length - 1];
const memoryUsagePercent = (latestMetrics.memoryUsage.heapUsed / latestMetrics.memoryUsage.heapTotal) * 100;
if (memoryUsagePercent > 90) {
alerts.push('CRITICAL: Memory usage above 90%');
}
if (latestMetrics.systemLoad[0] > os.cpus().length) {
alerts.push('WARNING: System load exceeds CPU core count');
}
return alerts;
}
}Die Integration von Performance Profiling in Ihre bestehende NestJS-Anwendung sollte so nahtlos wie möglich erfolgen. Ein Decorator-basierter Ansatz ermöglicht es Ihnen, Profiling selektiv auf kritische Methoden anzuwenden, ohne den bestehenden Code zu verändern.
Memory Leaks sind wie undichte Rohre in Ihrem Haus - sie mögen anfangs unbemerkt bleiben, aber über Zeit können sie ernsthafte Schäden verursachen. In Node.js-Anwendungen können Memory Leaks dazu führen, dass der Speicherverbrauch kontinuierlich steigt, bis die Anwendung schließlich abstürzt oder vom System beendet wird.
Das Tückische an Memory Leaks ist, dass sie oft erst in der Produktion unter realen Lasten sichtbar werden. In der Entwicklungsumgebung, wo Requests sporadisch sind und die Anwendung regelmäßig neu gestartet wird, bleiben sie meist unentdeckt.
Verstehen Sie zunächst, wie Garbage Collection in Node.js funktioniert. Node.js verwendet die V8 JavaScript Engine, die automatisch Speicher freigibt, der nicht mehr erreichbar ist. Ein Memory Leak entsteht, wenn Objekte im Speicher verbleiben, obwohl sie nicht mehr benötigt werden, aber trotzdem von irgendwo referenziert werden.
// memory-analysis/src/services/memory-leak-detector.service.ts
// Ein System zur Erkennung und Analyse von Memory Leaks
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import * as v8 from 'v8';
import * as fs from 'fs';
import * as path from 'path';
interface MemorySnapshot {
timestamp: Date;
heapUsed: number;
heapTotal: number;
external: number;
rss: number;
arrayBuffers: number;
objectCounts: Map<string, number>;
}
interface LeakDetectionResult {
isLeakDetected: boolean;
leakRate: number; // MB per hour
suspiciousObjects: string[];
recommendations: string[];
snapshotPath?: string;
}
@Injectable()
export class MemoryLeakDetectorService implements OnModuleInit {
private readonly logger = new Logger(MemoryLeakDetectorService.name);
// Speichert Memory-Snapshots über Zeit
private readonly memorySnapshots: MemorySnapshot[] = [];
private readonly maxSnapshots = 100;
// Tracking für spezifische Objekt-Typen
private readonly objectCounters = new Map<string, number>();
// Konfiguration für Leak-Detection
private readonly leakDetectionConfig = {
snapshotInterval: 5 * 60 * 1000, // 5 Minuten
leakThreshold: 10, // MB pro Stunde
minSnapshotsForDetection: 6, // Mindestens 30 Minuten Daten
};
async onModuleInit() {
// Starte automatische Memory-Überwachung
this.startMemoryMonitoring();
// Registriere Process-Event-Handler für Speicher-Warnungen
this.registerMemoryWarningHandlers();
}
/**
* Erstellt einen detaillierten Memory-Snapshot
* Diese Methode sammelt umfassende Informationen über den aktuellen Speicherzustand
*/
async createMemorySnapshot(): Promise<MemorySnapshot> {
// Force Garbage Collection für genauere Messungen (nur in Development)
if (process.env.NODE_ENV === 'development' && global.gc) {
global.gc();
}
const memoryUsage = process.memoryUsage();
const heapStatistics = v8.getHeapStatistics();
// Analysiere Objekt-Typen im Heap
const objectCounts = await this.analyzeObjectTypes();
const snapshot: MemorySnapshot = {
timestamp: new Date(),
heapUsed: memoryUsage.heapUsed,
heapTotal: memoryUsage.heapTotal,
external: memoryUsage.external,
rss: memoryUsage.rss,
arrayBuffers: memoryUsage.arrayBuffers,
objectCounts,
};
// Speichere Snapshot in der Historie
this.memorySnapshots.push(snapshot);
// Halte nur die letzten N Snapshots
if (this.memorySnapshots.length > this.maxSnapshots) {
this.memorySnapshots.shift();
}
this.logger.debug('Memory snapshot created', {
heapUsedMB: (snapshot.heapUsed / 1024 / 1024).toFixed(2),
heapTotalMB: (snapshot.heapTotal / 1024 / 1024).toFixed(2),
rssMB: (snapshot.rss / 1024 / 1024).toFixed(2),
});
return snapshot;
}
/**
* Analysiert Memory-Trends und erkennt potentielle Leaks
* Verwendet statistische Analyse um zwischen normalen Schwankungen und echten Leaks zu unterscheiden
*/
async detectMemoryLeaks(): Promise<LeakDetectionResult> {
if (this.memorySnapshots.length < this.leakDetectionConfig.minSnapshotsForDetection) {
return {
isLeakDetected: false,
leakRate: 0,
suspiciousObjects: [],
recommendations: ['Insufficient data for leak detection - need at least 30 minutes of monitoring'],
};
}
// Analysiere Heap-Größen-Trend
const heapTrend = this.analyzeHeapTrend();
const objectGrowthAnalysis = this.analyzeObjectGrowth();
// Berechne Leak-Rate (MB pro Stunde)
const leakRate = this.calculateLeakRate();
const isLeakDetected = leakRate > this.leakDetectionConfig.leakThreshold;
let snapshotPath: string | undefined;
if (isLeakDetected) {
// Erstelle detaillierten Heap-Snapshot für weitere Analyse
snapshotPath = await this.createHeapSnapshot();
}
const recommendations = this.generateLeakRecommendations(heapTrend, objectGrowthAnalysis, leakRate);
return {
isLeakDetected,
leakRate,
suspiciousObjects: objectGrowthAnalysis.rapidlyGrowingObjects,
recommendations,
snapshotPath,
};
}
/**
* Überwacht spezifische Objekt-Instanzen
* Nützlich für das Tracking von bekannten problematischen Objekten
*/
trackObjectInstances(objectName: string, currentCount: number): void {
const previousCount = this.objectCounters.get(objectName) || 0;
this.objectCounters.set(objectName, currentCount);
// Warne bei ungewöhnlichem Anstieg
if (currentCount > previousCount * 2 && previousCount > 0) {
this.logger.warn(`Rapid increase in ${objectName} instances detected`, {
previous: previousCount,
current: currentCount,
increasePercent: ((currentCount - previousCount) / previousCount * 100).toFixed(1),
});
}
}
/**
* Erstellt einen detaillierten Report über den Speicherzustand
* Hilfreich für die Analyse und das Debugging von Memory-Problemen
*/
generateMemoryReport(): {
currentState: MemorySnapshot;
trends: any;
suspiciousPatterns: string[];
historicalData: MemorySnapshot[];
} {
const currentState = this.memorySnapshots[this.memorySnapshots.length - 1];
const trends = this.analyzeMemoryTrends();
const suspiciousPatterns = this.identifySuspiciousPatterns();
return {
currentState,
trends,
suspiciousPatterns,
historicalData: this.memorySnapshots.slice(-20), // Letzte 20 Snapshots
};
}
private startMemoryMonitoring(): void {
// Automatische Snapshot-Erstellung
setInterval(async () => {
try {
await this.createMemorySnapshot();
// Periodische Leak-Detection
const leakDetection = await this.detectMemoryLeaks();
if (leakDetection.isLeakDetected) {
this.logger.error('Memory leak detected!', {
leakRate: leakDetection.leakRate,
suspiciousObjects: leakDetection.suspiciousObjects,
snapshotPath: leakDetection.snapshotPath,
});
}
} catch (error) {
this.logger.error('Error during memory monitoring:', error);
}
}, this.leakDetectionConfig.snapshotInterval);
}
private registerMemoryWarningHandlers(): void {
// Handler für Speicher-Warnungen des Systems
process.on('warning', (warning) => {
if (warning.name === 'MaxListenersExceededWarning') {
this.logger.warn('Potential EventEmitter memory leak detected', {
warning: warning.message,
});
}
});
// Handler für unkritische Speicher-Druck-Situationen
if (process.memoryUsage && typeof process.memoryUsage === 'function') {
setInterval(() => {
const usage = process.memoryUsage();
const heapUsagePercent = (usage.heapUsed / usage.heapTotal) * 100;
if (heapUsagePercent > 85) {
this.logger.warn('High heap usage detected', {
heapUsagePercent: heapUsagePercent.toFixed(1),
heapUsedMB: (usage.heapUsed / 1024 / 1024).toFixed(2),
heapTotalMB: (usage.heapTotal / 1024 / 1024).toFixed(2),
});
}
}, 60000); // Prüfe jede Minute
}
}
private async analyzeObjectTypes(): Promise<Map<string, number>> {
const objectCounts = new Map<string, number>();
// Diese Implementierung ist vereinfacht
// In einer produktiven Anwendung würden Sie Tools wie @memlab/core verwenden
// oder native V8-Profiling-APIs für detailliertere Objektanalyse
try {
const heapStats = v8.getHeapStatistics();
objectCounts.set('total_heap_size', heapStats.total_heap_size);
objectCounts.set('used_heap_size', heapStats.used_heap_size);
objectCounts.set('heap_size_limit', heapStats.heap_size_limit);
} catch (error) {
this.logger.error('Error analyzing object types:', error);
}
return objectCounts;
}
private analyzeHeapTrend(): { trend: 'increasing' | 'decreasing' | 'stable'; rate: number } {
if (this.memorySnapshots.length < 3) {
return { trend: 'stable', rate: 0 };
}
const recent = this.memorySnapshots.slice(-6); // Letzte 30 Minuten
const values = recent.map(s => s.heapUsed);
// Einfache lineare Regression für Trend-Analyse
const n = values.length;
const sumX = Array.from({ length: n }, (_, i) => i).reduce((a, b) => a + b, 0);
const sumY = values.reduce((a, b) => a + b, 0);
const sumXY = values.reduce((sum, y, x) => sum + x * y, 0);
const sumXX = Array.from({ length: n }, (_, i) => i * i).reduce((a, b) => a + b, 0);
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
if (Math.abs(slope) < 1000) { // Weniger als 1KB pro Snapshot
return { trend: 'stable', rate: slope };
}
return {
trend: slope > 0 ? 'increasing' : 'decreasing',
rate: slope,
};
}
private analyzeObjectGrowth(): { rapidlyGrowingObjects: string[] } {
const rapidlyGrowingObjects: string[] = [];
if (this.memorySnapshots.length < 2) {
return { rapidlyGrowingObjects };
}
const current = this.memorySnapshots[this.memorySnapshots.length - 1];
const previous = this.memorySnapshots[this.memorySnapshots.length - 2];
// Vergleiche Objekt-Counts zwischen Snapshots
for (const [objectType, currentCount] of current.objectCounts) {
const previousCount = previous.objectCounts.get(objectType) || 0;
if (previousCount > 0 && currentCount > previousCount * 1.5) {
rapidlyGrowingObjects.push(objectType);
}
}
return { rapidlyGrowingObjects };
}
private calculateLeakRate(): number {
if (this.memorySnapshots.length < 2) {
return 0;
}
const first = this.memorySnapshots[0];
const last = this.memorySnapshots[this.memorySnapshots.length - 1];
const timeDiffHours = (last.timestamp.getTime() - first.timestamp.getTime()) / (1000 * 60 * 60);
const memoryDiffMB = (last.heapUsed - first.heapUsed) / (1024 * 1024);
return timeDiffHours > 0 ? memoryDiffMB / timeDiffHours : 0;
}
private async createHeapSnapshot(): Promise<string> {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `heap-snapshot-${timestamp}.heapsnapshot`;
const filepath = path.join(process.cwd(), 'logs', filename);
try {
// Stelle sicher, dass das Logs-Verzeichnis existiert
const logsDir = path.dirname(filepath);
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
// Erstelle Heap-Snapshot
const heapSnapshot = v8.getHeapSnapshot();
const writeStream = fs.createWriteStream(filepath);
heapSnapshot.pipe(writeStream);
return new Promise((resolve, reject) => {
writeStream.on('finish', () => resolve(filepath));
writeStream.on('error', reject);
});
} catch (error) {
this.logger.error('Failed to create heap snapshot:', error);
throw error;
}
}
private generateLeakRecommendations(
heapTrend: any,
objectGrowth: any,
leakRate: number
): string[] {
const recommendations: string[] = [];
if (leakRate > 20) {
recommendations.push('CRITICAL: High memory leak rate detected - immediate investigation required');
} else if (leakRate > 10) {
recommendations.push('WARNING: Moderate memory leak detected - monitor closely');
}
if (heapTrend.trend === 'increasing') {
recommendations.push('Heap size is continuously growing - check for retained references');
}
if (objectGrowth.rapidlyGrowingObjects.length > 0) {
recommendations.push(`Investigate rapidly growing objects: ${objectGrowth.rapidlyGrowingObjects.join(', ')}`);
}
recommendations.push('Consider implementing explicit resource cleanup in finally blocks');
recommendations.push('Review EventEmitter usage for proper listener removal');
recommendations.push('Check for circular references in cached objects');
return recommendations;
}
private analyzeMemoryTrends(): any {
// Vereinfachte Trend-Analyse
return {
averageHeapUsage: this.memorySnapshots.reduce((sum, s) => sum + s.heapUsed, 0) / this.memorySnapshots.length,
peakHeapUsage: Math.max(...this.memorySnapshots.map(s => s.heapUsed)),
memoryGrowthRate: this.calculateLeakRate(),
};
}
private identifySuspiciousPatterns(): string[] {
const patterns: string[] = [];
if (this.memorySnapshots.length < 3) {
return patterns;
}
// Prüfe auf kontinuierliches Wachstum
const recentGrowth = this.memorySnapshots.slice(-5).every((snapshot, index, array) => {
return index === 0 || snapshot.heapUsed > array[index - 1].heapUsed;
});
if (recentGrowth) {
patterns.push('Continuous memory growth pattern detected');
}
// Prüfe auf Speicher-Spikes
const averageHeap = this.memorySnapshots.reduce((sum, s) => sum + s.heapUsed, 0) / this.memorySnapshots.length;
const hasSpikes = this.memorySnapshots.some(s => s.heapUsed > averageHeap * 2);
if (hasSpikes) {
patterns.push('Memory usage spikes detected');
}
return patterns;
}
}Datenbankabfragen sind oft die Achillesferse von Web-Anwendungen. Stellen Sie sich vor, Ihre Datenbank wäre eine Bibliothek und jede Abfrage ein Bibliothekar, der ein bestimmtes Buch suchen muss. Ein effizienter Bibliothekar weiß genau, wo er suchen muss und findet das Buch schnell. Ein ineffizienter Bibliothekar durchsucht wahllos alle Regale. In NestJS-Anwendungen können schlecht optimierte Datenbankabfragen ähnliche Probleme verursachen - sie verlangsamen nicht nur die spezifische Operation, sondern können das gesamte System beeinträchtigen.
Database Query Analysis geht über das einfache Loggen von SQL-Statements hinaus. Es bedeutet, zu verstehen, wie Ihre Abfragen ausgeführt werden, welche Ressourcen sie verbrauchen und wie sie sich unter verschiedenen Lasten verhalten. Moderne ORMs wie TypeORM abstrahieren zwar viele Details, aber diese Abstraktion kann auch dazu führen, dass ineffiziente Abfragen unbemerkt bleiben.
// database-analysis/src/services/query-analyzer.service.ts
// Ein umfassendes System zur Analyse von Datenbankabfragen
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, QueryRunner } from 'typeorm';
import { performance } from 'perf_hooks';
interface QueryAnalysis {
queryId: string;
sql: string;
parameters: any[];
executionTime: number;
timestamp: Date;
affectedRows?: number;
executionPlan?: any;
warnings: string[];
optimizationSuggestions: string[];
}
interface QueryPerformanceMetrics {
totalQueries: number;
averageExecutionTime: number;
slowestQueries: QueryAnalysis[];
mostFrequentQueries: Map<string, number>;
nPlusOneDetections: number;
}
@Injectable()
export class QueryAnalyzerService {
private readonly logger = new Logger(QueryAnalyzerService.name);
// Sammelt alle Query-Analysen für spätere Auswertung
private readonly queryHistory: QueryAnalysis[] = [];
private readonly maxHistorySize = 1000;
// Trackt Query-Häufigkeiten für N+1-Detection
private readonly queryFrequency = new Map<string, number>();
private readonly suspiciousQueryPatterns = new Set<string>();
constructor(private readonly dataSource: DataSource) {
this.setupQueryLogging();
}
/**
* Analysiert eine spezifische Datenbankabfrage
* Diese Methode führt eine detaillierte Analyse durch, einschließlich Execution Plan
*/
async analyzeQuery(sql: string, parameters: any[] = []): Promise<QueryAnalysis> {
const queryId = this.generateQueryId(sql, parameters);
const startTime = performance.now();
let queryRunner: QueryRunner | undefined;
try {
queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
// Führe die Abfrage aus und sammle Metriken
const result = await queryRunner.query(sql, parameters);
const executionTime = performance.now() - startTime;
// Hole Execution Plan (PostgreSQL-spezifisch, anpassbar für andere DBs)
const executionPlan = await this.getExecutionPlan(queryRunner, sql, parameters);
// Analysiere die Abfrage auf potentielle Probleme
const warnings = this.identifyQueryWarnings(sql, executionTime, result);
const optimizationSuggestions = this.generateOptimizationSuggestions(sql, executionPlan, executionTime);
const analysis: QueryAnalysis = {
queryId,
sql: this.normalizeQuery(sql),
parameters,
executionTime,
timestamp: new Date(),
affectedRows: Array.isArray(result) ? result.length : result.affectedRows,
executionPlan,
warnings,
optimizationSuggestions,
};
this.recordQueryAnalysis(analysis);
// Warne bei langsamen Abfragen
if (executionTime > 100) {
this.logger.warn(`Slow query detected (${executionTime.toFixed(2)}ms): ${this.normalizeQuery(sql)}`);
}
return analysis;
} catch (error) {
const executionTime = performance.now() - startTime;
this.logger.error(`Query analysis failed (${executionTime.toFixed(2)}ms): ${sql}`, error);
return {
queryId,
sql: this.normalizeQuery(sql),
parameters,
executionTime,
timestamp: new Date(),
warnings: [`Query execution failed: ${error.message}`],
optimizationSuggestions: [],
};
} finally {
if (queryRunner) {
await queryRunner.release();
}
}
}
/**
* Erkennt N+1-Query-Probleme durch Analyse von Query-Mustern
* N+1-Probleme sind einer der häufigsten Performance-Killer in ORM-basierten Anwendungen
*/
detectNPlusOneProblems(): {
detectedProblems: Array<{
pattern: string;
occurrences: number;
suggestedSolution: string;
}>;
totalNPlusOneQueries: number;
} {
const problems: Array<{ pattern: string; occurrences: number; suggestedSolution: string }> = [];
let totalNPlusOneQueries = 0;
// Analysiere Query-Muster in der letzten Zeit (z.B. letzte 100 Queries)
const recentQueries = this.queryHistory.slice(-100);
const queryPatterns = new Map<string, QueryAnalysis[]>();
// Gruppiere ähnliche Queries
for (const query of recentQueries) {
const pattern = this.extractQueryPattern(query.sql);
if (!queryPatterns.has(pattern)) {
queryPatterns.set(pattern, []);
}
queryPatterns.get(pattern)!.push(query);
}
// Suche nach verdächtigen Mustern
for (const [pattern, queries] of queryPatterns) {
// N+1-Indikatoren:
// 1. Viele ähnliche SELECT-Queries in kurzer Zeit
// 2. Queries, die nur durch Parameter unterscheiden
// 3. Queries mit WHERE-Klauseln auf Foreign Keys
if (queries.length > 5 && this.isSelectQuery(pattern) && this.hasForeignKeyFilter(pattern)) {
const timeDifference = queries[queries.length - 1].timestamp.getTime() - queries[0].timestamp.getTime();
// Wenn viele ähnliche Queries in weniger als 1 Sekunde ausgeführt wurden
if (timeDifference < 1000) {
problems.push({
pattern: pattern,
occurrences: queries.length,
suggestedSolution: this.suggestNPlusOneSolution(pattern),
});
totalNPlusOneQueries += queries.length;
}
}
}
return {
detectedProblems: problems,
totalNPlusOneQueries,
};
}
/**
* Generiert einen umfassenden Performance-Report
* Hilfreich für regelmäßige Performance-Reviews und Optimierungen
*/
generatePerformanceReport(): QueryPerformanceMetrics {
const totalQueries = this.queryHistory.length;
const averageExecutionTime = this.queryHistory.reduce((sum, q) => sum + q.executionTime, 0) / totalQueries;
// Die 10 langsamsten Queries
const slowestQueries = this.queryHistory
.slice() // Kopie für Sortierung
.sort((a, b) => b.executionTime - a.executionTime)
.slice(0, 10);
// Häufigste Query-Muster
const patternFrequency = new Map<string, number>();
for (const query of this.queryHistory) {
const pattern = this.extractQueryPattern(query.sql);
patternFrequency.set(pattern, (patternFrequency.get(pattern) || 0) + 1);
}
const nPlusOneDetection = this.detectNPlusOneProblems();
return {
totalQueries,
averageExecutionTime,
slowestQueries,
mostFrequentQueries: patternFrequency,
nPlusOneDetections: nPlusOneDetection.totalNPlusOneQueries,
};
}
/**
* Überwacht Query-Performance in Echtzeit
* Sendet Alerts bei kritischen Performance-Problemen
*/
async monitorQueryPerformance(): Promise<void> {
const recentQueries = this.queryHistory.slice(-50); // Letzte 50 Queries
if (recentQueries.length === 0) return;
const averageTime = recentQueries.reduce((sum, q) => sum + q.executionTime, 0) / recentQueries.length;
const slowQueries = recentQueries.filter(q => q.executionTime > 200);
// Alert bei erhöhter durchschnittlicher Query-Zeit
if (averageTime > 100) {
this.logger.warn(`High average query execution time detected: ${averageTime.toFixed(2)}ms`, {
recentQueriesCount: recentQueries.length,
slowQueriesCount: slowQueries.length,
});
}
// Alert bei vielen langsamen Queries
if (slowQueries.length > recentQueries.length * 0.2) {
this.logger.error(`High percentage of slow queries detected: ${((slowQueries.length / recentQueries.length) * 100).toFixed(1)}%`);
}
// N+1-Detection
const nPlusOneProblems = this.detectNPlusOneProblems();
if (nPlusOneProblems.detectedProblems.length > 0) {
this.logger.error(`N+1 query problems detected:`, nPlusOneProblems.detectedProblems);
}
}
private setupQueryLogging(): void {
// Diese Implementierung hängt vom verwendeten ORM ab
// Für TypeORM können Sie einen benutzerdefinierten Logger erstellen
const originalQuery = this.dataSource.query.bind(this.dataSource);
this.dataSource.query = async (sql: string, parameters?: any[]): Promise<any> => {
const analysis = await this.analyzeQuery(sql, parameters);
return originalQuery(sql, parameters);
};
}
private async getExecutionPlan(queryRunner: QueryRunner, sql: string, parameters: any[]): Promise<any> {
try {
// PostgreSQL EXPLAIN ANALYZE
const explainQuery = `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${sql}`;
const plan = await queryRunner.query(explainQuery, parameters);
return plan[0]['QUERY PLAN'][0];
} catch (error) {
// Fallback für Datenbanken, die EXPLAIN nicht unterstützen
return null;
}
}
private identifyQueryWarnings(sql: string, executionTime: number, result: any): string[] {
const warnings: string[] = [];
// Performance-Warnungen
if (executionTime > 500) {
warnings.push(`Very slow query: ${executionTime.toFixed(2)}ms execution time`);
}
// Query-Pattern-Warnungen
if (sql.toLowerCase().includes('select *')) {
warnings.push('Using SELECT * - consider specifying specific columns');
}
if (!sql.toLowerCase().includes('limit') && sql.toLowerCase().includes('select')) {
warnings.push('No LIMIT clause found - potential for large result sets');
}
if (sql.toLowerCase().includes('or')) {
warnings.push('OR clause detected - may prevent index usage');
}
// Große Ergebnismengen
if (Array.isArray(result) && result.length > 1000) {
warnings.push(`Large result set: ${result.length} rows returned`);
}
return warnings;
}
private generateOptimizationSuggestions(sql: string, executionPlan: any, executionTime: number): string[] {
const suggestions: string[] = [];
if (executionTime > 100) {
suggestions.push('Consider adding appropriate database indexes');
suggestions.push('Review WHERE clause for optimization opportunities');
}
if (executionPlan) {
// Analysiere Execution Plan für spezifische Optimierungen
if (this.hasSequentialScan(executionPlan)) {
suggestions.push('Sequential scan detected - consider adding index');
}
if (this.hasNestedLoop(executionPlan)) {
suggestions.push('Nested loop join detected - verify join conditions');
}
}
if (sql.toLowerCase().includes('like %')) {
suggestions.push('LIKE with leading wildcard prevents index usage - consider full-text search');
}
return suggestions;
}
private recordQueryAnalysis(analysis: QueryAnalysis): void {
this.queryHistory.push(analysis);
// Halte History-Größe begrenzt
if (this.queryHistory.length > this.maxHistorySize) {
this.queryHistory.shift();
}
// Update Query-Frequency-Tracking
const pattern = this.extractQueryPattern(analysis.sql);
this.queryFrequency.set(pattern, (this.queryFrequency.get(pattern) || 0) + 1);
}
private generateQueryId(sql: string, parameters: any[]): string {
const normalizedSql = this.normalizeQuery(sql);
const paramString = JSON.stringify(parameters);
return `query_${Date.now()}_${normalizedSql.substring(0, 50).replace(/\s+/g, '_')}`;
}
private normalizeQuery(sql: string): string {
return sql.replace(/\s+/g, ' ').trim();
}
private extractQueryPattern(sql: string): string {
// Ersetzt Parameter-Platzhalter für Pattern-Erkennung
return sql
.replace(/\$\d+/g, '?') // PostgreSQL-Parameter
.replace(/'[^']*'/g, '?') // String-Literals
.replace(/\b\d+\b/g, '?') // Numerische Literals
.replace(/\s+/g, ' ')
.trim();
}
private isSelectQuery(pattern: string): boolean {
return pattern.toLowerCase().startsWith('select');
}
private hasForeignKeyFilter(pattern: string): boolean {
// Vereinfachte Heuristik für Foreign Key-Filter
return /where.*_id\s*=\s*\?/i.test(pattern);
}
private suggestNPlusOneSolution(pattern: string): string {
if (pattern.includes('JOIN')) {
return 'Consider using eager loading with JOIN FETCH or relations in TypeORM';
}
return 'Use batch loading or eager loading to reduce N+1 queries';
}
private hasSequentialScan(plan: any): boolean {
if (!plan || !plan.Plan) return false;
return plan.Plan['Node Type'] === 'Seq Scan';
}
private hasNestedLoop(plan: any): boolean {
if (!plan || !plan.Plan) return false;
return plan.Plan['Node Type'] === 'Nested Loop';
}
}In modernen Mikroservice-Architekturen ist eine einzelne Benutzeranfrage wie ein Staffellauf - sie wird von einem Service zum nächsten weitergegeben, wobei jeder Service einen Teil der Arbeit erledigt. Distributed Tracing ist wie ein GPS-System, das den gesamten Weg dieser Anfrage verfolgt und Ihnen zeigt, wo sie war, wie lange sie an jedem Punkt verbracht hat und wo möglicherweise Probleme aufgetreten sind.
Stellen Sie sich vor, Sie versuchen herauszufinden, warum eine Online-Bestellung langsam verarbeitet wird. Die Anfrage könnte durch einen Authentifizierungs-Service, einen Produktkatalog-Service, einen Inventory-Service, einen Payment-Service und schließlich einen Fulfillment-Service laufen. Ohne Distributed Tracing wäre es wie das Suchen nach einer Nadel im Heuhaufen - Sie wüssten nicht, welcher Service das Problem verursacht.
// tracing/src/services/distributed-tracer.service.ts
// Ein umfassendes Distributed Tracing-System für NestJS-Anwendungen
import { Injectable, Logger } from '@nestjs/common';
import { AsyncLocalStorage } from 'async_hooks';
import { v4 as uuidv4 } from 'uuid';
interface TraceContext {
traceId: string;
spanId: string;
parentSpanId?: string;
baggage?: Record<string, string>;
startTime: bigint;
serviceName: string;
operationName: string;
}
interface Span {
traceId: string;
spanId: string;
parentSpanId?: string;
serviceName: string;
operationName: string;
startTime: bigint;
endTime?: bigint;
duration?: number;
tags: Record<string, any>;
logs: Array<{ timestamp: bigint; fields: Record<string, any> }>;
status: 'ok' | 'error' | 'timeout';
errorMessage?: string;
}
@Injectable()
export class DistributedTracerService {
private readonly logger = new Logger(DistributedTracerService.name);
// Async Local Storage für Trace-Kontext-Propagation
private readonly asyncLocalStorage = new AsyncLocalStorage<TraceContext>();
// Speichert abgeschlossene Spans für Export
private readonly completedSpans: Span[] = [];
private readonly maxSpansInMemory = 10000;
// Service-Name für diesen Service
private readonly serviceName = process.env.SERVICE_NAME || 'nestjs-app';
/**
* Startet eine neue Trace oder erstellt einen Child-Span
* Diese Methode ist der Einstiegspunkt für alle Tracing-Operationen
*/
startSpan(operationName: string, parentContext?: TraceContext): TraceContext {
const spanId = this.generateSpanId();
const traceId = parentContext?.traceId || this.generateTraceId();
const parentSpanId = parentContext?.spanId;
const context: TraceContext = {
traceId,
spanId,
parentSpanId,
baggage: parentContext?.baggage || {},
startTime: process.hrtime.bigint(),
serviceName: this.serviceName,
operationName,
};
this.logger.debug(`Started span: ${operationName}`, {
traceId,
spanId,
parentSpanId,
});
return context;
}
/**
* Beendet einen Span und protokolliert die Ergebnisse
* Sammelt alle relevanten Informationen für die spätere Analyse
*/
finishSpan(
context: TraceContext,
tags: Record<string, any> = {},
error?: Error
): Span {
const endTime = process.hrtime.bigint();
const duration = Number(endTime - context.startTime) / 1000000; // Konvertiere zu Millisekunden
const span: Span = {
traceId: context.traceId,
spanId: context.spanId,
parentSpanId: context.parentSpanId,
serviceName: context.serviceName,
operationName: context.operationName,
startTime: context.startTime,
endTime,
duration,
tags: {
'service.name': context.serviceName,
'span.kind': this.inferSpanKind(context.operationName),
...tags,
},
logs: [],
status: error ? 'error' : 'ok',
errorMessage: error?.message,
};
// Error-spezifische Tags hinzufügen
if (error) {
span.tags['error'] = true;
span.tags['error.type'] = error.name;
span.tags['error.message'] = error.message;
if (error.stack) {
span.tags['error.stack'] = error.stack;
}
}
this.recordSpan(span);
this.logger.debug(`Finished span: ${context.operationName} (${duration.toFixed(2)}ms)`, {
traceId: context.traceId,
spanId: context.spanId,
duration,
status: span.status,
});
return span;
}
/**
* Führt eine Operation mit automatischem Tracing aus
* Diese Utility-Methode vereinfacht das Tracing für einfache Operationen
*/
async traceOperation<T>(
operationName: string,
operation: (context: TraceContext) => Promise<T>,
tags: Record<string, any> = {}
): Promise<T> {
const context = this.startSpan(operationName);
try {
// Führe die Operation im Trace-Kontext aus
const result = await this.asyncLocalStorage.run(context, async () => {
return await operation(context);
});
this.finishSpan(context, tags);
return result;
} catch (error) {
this.finishSpan(context, tags, error);
throw error;
}
}
/**
* Fügt strukturierte Logs zu einem aktiven Span hinzu
* Hilfreich für das Protokollieren wichtiger Ereignisse während der Span-Ausführung
*/
logToSpan(fields: Record<string, any>, context?: TraceContext): void {
const spanContext = context || this.getCurrentContext();
if (!spanContext) {
this.logger.warn('Attempted to log to span, but no active span found');
return;
}
// Finde den entsprechenden Span (wenn er noch aktiv ist)
// In einer vollständigen Implementierung würden Sie aktive Spans tracken
this.logger.debug(`Span log: ${spanContext.operationName}`, {
traceId: spanContext.traceId,
spanId: spanContext.spanId,
...fields,
});
}
/**
* Extrahiert Trace-Kontext aus HTTP-Headern
* Implementiert W3C Trace Context Specification
*/
extractTraceContext(headers: Record<string, string>): TraceContext | null {
const traceparent = headers['traceparent'] || headers['x-trace-id'];
if (!traceparent) {
return null;
}
// Parse W3C traceparent header: version-trace_id-parent_id-trace_flags
const parts = traceparent.split('-');
if (parts.length !== 4) {
this.logger.warn(`Invalid traceparent header format: ${traceparent}`);
return null;
}
const [version, traceId, parentSpanId, flags] = parts;
// Extrahiere Baggage (falls vorhanden)
const baggage: Record<string, string> = {};
const baggageHeader = headers['baggage'];
if (baggageHeader) {
const baggageItems = baggageHeader.split(',');
for (const item of baggageItems) {
const [key, value] = item.trim().split('=');
if (key && value) {
baggage[key] = decodeURIComponent(value);
}
}
}
return {
traceId,
spanId: this.generateSpanId(), // Neuer Span für diesen Service
parentSpanId,
baggage,
startTime: process.hrtime.bigint(),
serviceName: this.serviceName,
operationName: 'http_request',
};
}
/**
* Injiziert Trace-Kontext in HTTP-Headers für ausgehende Requests
* Sorgt für Kontext-Propagation zwischen Services
*/
injectTraceContext(context: TraceContext): Record<string, string> {
const headers: Record<string, string> = {};
// W3C Trace Context
headers['traceparent'] = `00-${context.traceId}-${context.spanId}-01`;
// Baggage propagieren
if (context.baggage && Object.keys(context.baggage).length > 0) {
const baggageItems = Object.entries(context.baggage)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join(',');
headers['baggage'] = baggageItems;
}
// Custom Headers für Debugging
headers['x-trace-id'] = context.traceId;
headers['x-span-id'] = context.spanId;
headers['x-service-name'] = context.serviceName;
return headers;
}
/**
* Analysiert Trace-Daten für Performance-Insights
* Identifiziert Bottlenecks und Anomalien in der Service-Kommunikation
*/
analyzeTracePerformance(traceId: string): {
totalDuration: number;
criticalPath: Span[];
bottlenecks: Span[];
serviceBreakdown: Record<string, number>;
anomalies: string[];
} {
const traceSpans = this.completedSpans.filter(span => span.traceId === traceId);
if (traceSpans.length === 0) {
throw new Error(`No spans found for trace ${traceId}`);
}
// Sortiere Spans nach Start-Zeit
traceSpans.sort((a, b) => Number(a.startTime - b.startTime));
const totalDuration = Math.max(...traceSpans.map(span => span.duration || 0));
// Identifiziere Critical Path (längste Kette von abhängigen Spans)
const criticalPath = this.findCriticalPath(traceSpans);
// Identifiziere Bottlenecks (langsamste Spans)
const bottlenecks = traceSpans
.filter(span => span.duration && span.duration > totalDuration * 0.2)
.sort((a, b) => (b.duration || 0) - (a.duration || 0));
// Service-Performance-Aufschlüsselung
const serviceBreakdown: Record<string, number> = {};
for (const span of traceSpans) {
if (span.duration) {
serviceBreakdown[span.serviceName] = (serviceBreakdown[span.serviceName] || 0) + span.duration;
}
}
// Identifiziere Anomalien
const anomalies = this.identifyTraceAnomalies(traceSpans, totalDuration);
return {
totalDuration,
criticalPath,
bottlenecks,
serviceBreakdown,
anomalies,
};
}
/**
* Holt den aktuellen Trace-Kontext aus dem AsyncLocalStorage
*/
getCurrentContext(): TraceContext | undefined {
return this.asyncLocalStorage.getStore();
}
/**
* Exportiert Trace-Daten für externe Analyse-Tools
* Kompatibel mit OpenTelemetry und Jaeger
*/
exportTraces(format: 'jaeger' | 'zipkin' | 'otlp' = 'jaeger'): any[] {
const exports: any[] = [];
for (const span of this.completedSpans) {
let exportedSpan: any;
switch (format) {
case 'jaeger':
exportedSpan = this.convertToJaegerFormat(span);
break;
case 'zipkin':
exportedSpan = this.convertToZipkinFormat(span);
break;
case 'otlp':
exportedSpan = this.convertToOTLPFormat(span);
break;
}
exports.push(exportedSpan);
}
return exports;
}
private generateTraceId(): string {
return uuidv4().replace(/-/g, '');
}
private generateSpanId(): string {
return Math.random().toString(16).substr(2, 16);
}
private inferSpanKind(operationName: string): string {
if (operationName.includes('http') || operationName.includes('request')) {
return 'client';
}
if (operationName.includes('database') || operationName.includes('query')) {
return 'client';
}
if (operationName.includes('handler') || operationName.includes('controller')) {
return 'server';
}
return 'internal';
}
private recordSpan(span: Span): void {
this.completedSpans.push(span);
// Halte Speicherverbrauch begrenzt
if (this.completedSpans.length > this.maxSpansInMemory) {
// Entferne die ältesten 10% der Spans
const toRemove = Math.floor(this.maxSpansInMemory * 0.1);
this.completedSpans.splice(0, toRemove);
}
// Potentiell: Export zu externem Tracing-System
// this.exportSpanToExternalSystem(span);
}
private findCriticalPath(spans: Span[]): Span[] {
// Vereinfachte Critical Path-Analyse
// In einer vollständigen Implementierung würden Sie Span-Abhängigkeiten verfolgen
return spans
.filter(span => span.duration && span.duration > 0)
.sort((a, b) => (b.duration || 0) - (a.duration || 0))
.slice(0, 5); // Top 5 längste Spans als Approximation
}
private identifyTraceAnomalies(spans: Span[], totalDuration: number): string[] {
const anomalies: string[] = [];
// Lange Gaps zwischen Spans
for (let i = 1; i < spans.length; i++) {
const gap = Number(spans[i].startTime - spans[i-1].startTime) / 1000000;
if (gap > totalDuration * 0.1) {
anomalies.push(`Large gap detected between spans: ${gap.toFixed(2)}ms`);
}
}
// Error-Rate-Analyse
const errorSpans = spans.filter(span => span.status === 'error');
if (errorSpans.length > spans.length * 0.1) {
anomalies.push(`High error rate: ${((errorSpans.length / spans.length) * 100).toFixed(1)}%`);
}
// Ungewöhnlich langsame Spans
const averageDuration = spans.reduce((sum, span) => sum + (span.duration || 0), 0) / spans.length;
const slowSpans = spans.filter(span => span.duration && span.duration > averageDuration * 3);
if (slowSpans.length > 0) {
anomalies.push(`${slowSpans.length} spans significantly slower than average`);
}
return anomalies;
}
private convertToJaegerFormat(span: Span): any {
return {
traceID: span.traceId,
spanID: span.spanId,
parentSpanID: span.parentSpanId,
operationName: span.operationName,
startTime: Number(span.startTime) / 1000, // Konvertiere zu Mikrosekunden
duration: (span.duration || 0) * 1000, // Konvertiere zu Mikrosekunden
tags: Object.entries(span.tags).map(([key, value]) => ({
key,
type: typeof value === 'string' ? 'string' : 'number',
value: String(value),
})),
process: {
serviceName: span.serviceName,
tags: [],
},
};
}
private convertToZipkinFormat(span: Span): any {
return {
traceId: span.traceId,
id: span.spanId,
parentId: span.parentSpanId,
name: span.operationName,
timestamp: Number(span.startTime) / 1000, // Mikrosekunden
duration: (span.duration || 0) * 1000, // Mikrosekunden
localEndpoint: {
serviceName: span.serviceName,
},
tags: span.tags,
};
}
private convertToOTLPFormat(span: Span): any {
return {
traceId: span.traceId,
spanId: span.spanId,
parentSpanId: span.parentSpanId,
name: span.operationName,
startTimeUnixNano: String(span.startTime),
endTimeUnixNano: span.endTime ? String(span.endTime) : undefined,
attributes: Object.entries(span.tags).map(([key, value]) => ({
key,
value: { stringValue: String(value) },
})),
status: {
code: span.status === 'ok' ? 1 : 2,
message: span.errorMessage,
},
};
}
}Production Debugging ist wie Herzchirurgie am offenen Herzen - Sie müssen äußerst vorsichtig vorgehen, da jeder Fehler katastrophale Auswirkungen haben kann. In der Produktion haben Sie nicht den Luxus, die Anwendung zu stoppen, Debugger anzuhängen oder experimentelle Änderungen vorzunehmen. Sie müssen mit den verfügbaren Informationen arbeiten und dabei sicherstellen, dass Sie das System nicht weiter destabilisieren.
Der Schlüssel zu erfolgreichem Production Debugging liegt in der Vorbereitung. Wie ein Feuerwehrmann, der seine Ausrüstung vorbereitet hat, bevor der Alarm ertönt, müssen Sie Ihre Debugging-Infrastruktur einrichten, bevor Probleme auftreten. Das bedeutet: umfassende Logging-Systeme, Monitoring-Dashboards, Health Checks und die Fähigkeit, problematische Features schnell zu deaktivieren.
// production-debugging/src/services/production-debugger.service.ts
// Ein sicheres und umfassendes System für Production Debugging
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { readFileSync, writeFileSync, existsSync } from 'fs';
import * as path from 'path';
interface DebugSession {
sessionId: string;
startTime: Date;
endTime?: Date;
debugLevel: 'minimal' | 'standard' | 'verbose';
targetModules: string[];
collectedData: any[];
safetyChecks: string[];
initiatedBy: string;
}
interface ProductionIssue {
issueId: string;
severity: 'low' | 'medium' | 'high' | 'critical';
category: 'performance' | 'error' | 'availability' | 'security';
description: string;
affectedComponents: string[];
detectionTime: Date;
status: 'detected' | 'investigating' | 'mitigating' | 'resolved';
debugActions: string[];
evidence: any[];
}
@Injectable()
export class ProductionDebuggerService {
private readonly logger = new Logger(ProductionDebuggerService.name);
// Aktive Debug-Sessions
private readonly activeSessions = new Map<string, DebugSession>();
// Erkannte Produktionsprobleme
private readonly activeIssues = new Map<string, ProductionIssue>();
// Sichere Debug-Konfiguration
private readonly debugConfig = {
maxConcurrentSessions: 2,
maxSessionDuration: 30 * 60 * 1000, // 30 Minuten
allowedDebugLevels: ['minimal', 'standard'] as const,
restrictedModules: ['auth', 'payment', 'security'],
maxDataCollectionSize: 100 * 1024 * 1024, // 100MB
};
constructor(private readonly configService: ConfigService) {
this.initializeProductionSafeguards();
}
/**
* Startet eine sichere Debug-Session in der Produktion
* Implementiert strenge Sicherheitskontrollen und Ressourcenlimits
*/
async startDebugSession(
targetModules: string[],
debugLevel: 'minimal' | 'standard' | 'verbose',
initiatedBy: string,
duration: number = 15 * 60 * 1000 // Standard: 15 Minuten
): Promise<string> {
// Sicherheitsprüfungen vor dem Start
const safetyChecks = await this.performSafetyChecks(targetModules, debugLevel, duration);
if (safetyChecks.length > 0) {
throw new Error(`Debug session rejected due to safety concerns: ${safetyChecks.join(', ')}`);
}
const sessionId = this.generateSessionId();
const session: DebugSession = {
sessionId,
startTime: new Date(),
debugLevel: this.sanitizeDebugLevel(debugLevel),
targetModules: this.sanitizeTargetModules(targetModules),
collectedData: [],
safetyChecks: [],
initiatedBy,
};
this.activeSessions.set(sessionId, session);
// Automatisches Session-Timeout
setTimeout(() => {
this.endDebugSession(sessionId, 'timeout');
}, Math.min(duration, this.debugConfig.maxSessionDuration));
this.logger.warn(`Production debug session started`, {
sessionId,
targetModules: session.targetModules,
debugLevel: session.debugLevel,
initiatedBy,
maxDuration: duration,
});
return sessionId;
}
/**
* Sammelt Debug-Informationen während einer aktiven Session
* Respektiert Sicherheitsgrenzen und Datenschutz
*/
async collectDebugData(
sessionId: string,
dataType: 'logs' | 'metrics' | 'state' | 'traces',
filters?: Record<string, any>
): Promise<any> {
const session = this.activeSessions.get(sessionId);
if (!session) {
throw new Error(`No active debug session found: ${sessionId}`);
}
let collectedData: any;
switch (dataType) {
case 'logs':
collectedData = await this.collectSecureLogs(session, filters);
break;
case 'metrics':
collectedData = await this.collectSystemMetrics(session, filters);
break;
case 'state':
collectedData = await this.collectApplicationState(session, filters);
break;
case 'traces':
collectedData = await this.collectTraceData(session, filters);
break;
default:
throw new Error(`Unsupported data type: ${dataType}`);
}
// Prüfe Datengröße-Limits
const dataSize = JSON.stringify(collectedData).length;
const totalCollectedSize = session.collectedData.reduce((sum, data) => sum + JSON.stringify(data).length, 0);
if (totalCollectedSize + dataSize > this.debugConfig.maxDataCollectionSize) {
this.logger.warn(`Debug data collection limit reached for session ${sessionId}`);
return { error: 'Data collection limit reached' };
}
// Sanitize sensitive data
const sanitizedData = this.sanitizeDebugData(collectedData);
session.collectedData.push({
timestamp: new Date(),
dataType,
data: sanitizedData,
filters,
});
this.logger.debug(`Debug data collected`, {
sessionId,
dataType,
dataSize,
totalCollectedSize: totalCollectedSize + dataSize,
});
return sanitizedData;
}
/**
* Erstellt einen sicheren Debug-Report für Produktionsprobleme
* Entfernt sensible Daten und strukturiert Informationen für die Analyse
*/
async generateDebugReport(sessionId: string): Promise<{
sessionSummary: any;
collectedData: any[];
analysisResults: any;
recommendations: string[];
}> {
const session = this.activeSessions.get(sessionId);
if (!session) {
throw new Error(`Debug session not found: ${sessionId}`);
}
const sessionSummary = {
sessionId: session.sessionId,
duration: session.endTime
? session.endTime.getTime() - session.startTime.getTime()
: Date.now() - session.startTime.getTime(),
debugLevel: session.debugLevel,
targetModules: session.targetModules,
dataCollectionCount: session.collectedData.length,
initiatedBy: session.initiatedBy,
};
// Analysiere gesammelte Daten
const analysisResults = this.analyzeCollectedData(session.collectedData);
// Generiere Empfehlungen basierend auf den Findings
const recommendations = this.generateDebugRecommendations(analysisResults);
return {
sessionSummary,
collectedData: session.collectedData,
analysisResults,
recommendations,
};
}
/**
* Implementiert einen Circuit Breaker für problematische Components
* Ermöglicht schnelle Deaktivierung von Features ohne Deployment
*/
async enableCircuitBreaker(
componentName: string,
reason: string,
duration: number = 60 * 60 * 1000 // 1 Stunde
): Promise<void> {
const circuitBreakerState = {
componentName,
disabled: true,
reason,
disabledAt: new Date(),
duration,
disabledBy: 'production-debugger',
};
// Speichere Circuit Breaker State persistent
await this.persistCircuitBreakerState(componentName, circuitBreakerState);
// Automatische Reaktivierung nach Duration
setTimeout(async () => {
await this.disableCircuitBreaker(componentName, 'automatic-timeout');
}, duration);
this.logger.error(`Circuit breaker activated for ${componentName}`, {
reason,
duration,
disabledAt: circuitBreakerState.disabledAt,
});
}
/**
* Überprüft Circuit Breaker Status für einen Component
* Wird von der Anwendungslogik verwendet um zu entscheiden, ob ein Feature verfügbar ist
*/
async isCircuitBreakerOpen(componentName: string): Promise<boolean> {
try {
const stateFilePath = path.join(process.cwd(), 'circuit-breakers', `${componentName}.json`);
if (!existsSync(stateFilePath)) {
return false;
}
const stateData = readFileSync(stateFilePath, 'utf8');
const state = JSON.parse(stateData);
// Prüfe ob Circuit Breaker noch aktiv ist
if (state.disabled) {
const now = new Date();
const disabledUntil = new Date(state.disabledAt.getTime() + state.duration);
if (now < disabledUntil) {
return true;
} else {
// Automatisch deaktivieren wenn Zeit abgelaufen
await this.disableCircuitBreaker(componentName, 'automatic-timeout');
return false;
}
}
return false;
} catch (error) {
this.logger.error(`Error checking circuit breaker for ${componentName}:`, error);
return false; // Fail-open für bessere Verfügbarkeit
}
}
/**
* Notfall-Debugging-Modus für kritische Produktionsprobleme
* Aktiviert erweiterte Logging und Monitoring mit minimaler Performance-Impact
*/
async activateEmergencyMode(
reason: string,
duration: number = 30 * 60 * 1000 // 30 Minuten
): Promise<void> {
const emergencySession = await this.startDebugSession(
['*'], // Alle Module
'standard',
'emergency-system',
duration
);
// Erhöhe Log-Level temporär
this.logger.debug('Emergency debugging mode activated', {
reason,
duration,
sessionId: emergencySession,
});
// Aktiviere erweiterte Metriken-Sammlung
this.enableEnhancedMetrics(duration);
// Benachrichtige Operations-Team
await this.notifyOperationsTeam({
type: 'emergency-debug-mode',
reason,
sessionId: emergencySession,
duration,
});
}
private async performSafetyChecks(
targetModules: string[],
debugLevel: string,
duration: number
): Promise<string[]> {
const issues: string[] = [];
// Prüfe maximale gleichzeitige Sessions
if (this.activeSessions.size >= this.debugConfig.maxConcurrentSessions) {
issues.push('Maximum concurrent debug sessions reached');
}
// Prüfe Debug-Level-Berechtigung
if (!this.debugConfig.allowedDebugLevels.includes(debugLevel as any)) {
issues.push(`Debug level '${debugLevel}' not allowed in production`);
}
// Prüfe eingeschränkte Module
const restrictedModulesRequested = targetModules.filter(module =>
this.debugConfig.restrictedModules.includes(module)
);
if (restrictedModulesRequested.length > 0) {
issues.push(`Restricted modules requested: ${restrictedModulesRequested.join(', ')}`);
}
// Prüfe System-Load
const currentLoad = await this.getSystemLoad();
if (currentLoad > 0.8) {
issues.push(`High system load detected: ${(currentLoad * 100).toFixed(1)}%`);
}
// Prüfe maximale Session-Dauer
if (duration > this.debugConfig.maxSessionDuration) {
issues.push(`Requested duration exceeds maximum allowed: ${duration}ms > ${this.debugConfig.maxSessionDuration}ms`);
}
return issues;
}
private sanitizeDebugLevel(debugLevel: string): 'minimal' | 'standard' {
if (debugLevel === 'verbose' && !this.configService.get<boolean>('ALLOW_VERBOSE_DEBUG', false)) {
this.logger.warn('Verbose debug level requested but not allowed, downgrading to standard');
return 'standard';
}
return debugLevel as 'minimal' | 'standard';
}
private sanitizeTargetModules(modules: string[]): string[] {
return modules.filter(module =>
!this.debugConfig.restrictedModules.includes(module)
);
}
private sanitizeDebugData(data: any): any {
// Entferne sensible Felder rekursiv
const sensitiveFields = [
'password', 'token', 'secret', 'key', 'auth', 'credential',
'ssn', 'creditcard', 'cvv', 'pin', 'private'
];
const sanitize = (obj: any): any => {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(sanitize);
}
const sanitized: any = {};
for (const [key, value] of Object.entries(obj)) {
const lowerKey = key.toLowerCase();
const isSensitive = sensitiveFields.some(field => lowerKey.includes(field));
if (isSensitive) {
sanitized[key] = '[REDACTED]';
} else {
sanitized[key] = sanitize(value);
}
}
return sanitized;
};
return sanitize(data);
}
private generateSessionId(): string {
return `debug_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private async getSystemLoad(): Promise<number> {
// Vereinfachte System-Load-Berechnung
const memoryUsage = process.memoryUsage();
return memoryUsage.heapUsed / memoryUsage.heapTotal;
}
private initializeProductionSafeguards(): void {
// Verhindere versehentliche Debug-Aktivierung in Produktion
if (this.configService.get<string>('NODE_ENV') === 'production') {
this.logger.warn('Production debugging service initialized with safety constraints');
}
// Cleanup von alten Debug-Sessions beim Start
this.cleanupExpiredSessions();
}
private cleanupExpiredSessions(): void {
setInterval(() => {
const now = Date.now();
for (const [sessionId, session] of this.activeSessions) {
const sessionAge = now - session.startTime.getTime();
if (sessionAge > this.debugConfig.maxSessionDuration) {
this.endDebugSession(sessionId, 'expired');
}
}
}, 60000); // Prüfe jede Minute
}
private async endDebugSession(sessionId: string, reason: string): Promise<void> {
const session = this.activeSessions.get(sessionId);
if (session) {
session.endTime = new Date();
this.activeSessions.delete(sessionId);
this.logger.warn(`Debug session ended`, {
sessionId,
reason,
duration: session.endTime.getTime() - session.startTime.getTime(),
});
}
}
// Weitere private Methoden für spezifische Debug-Datensammlung...
private async collectSecureLogs(session: DebugSession, filters?: any): Promise<any> {
// Implementierung für sichere Log-Sammlung
return { logs: 'Implementation pending' };
}
private async collectSystemMetrics(session: DebugSession, filters?: any): Promise<any> {
// Implementierung für System-Metriken
return { metrics: 'Implementation pending' };
}
private async collectApplicationState(session: DebugSession, filters?: any): Promise<any> {
// Implementierung für Anwendungsstatus
return { state: 'Implementation pending' };
}
private async collectTraceData(session: DebugSession, filters?: any): Promise<any> {
// Implementierung für Trace-Daten
return { traces: 'Implementation pending' };
}
private analyzeCollectedData(collectedData: any[]): any {
// Implementierung für Datenanalyse
return { analysis: 'Implementation pending' };
}
private generateDebugRecommendations(analysisResults: any): string[] {
// Implementierung für Empfehlungen
return ['Implementation pending'];
}
private async persistCircuitBreakerState(componentName: string, state: any): Promise<void> {
// Implementierung für persistente Speicherung
}
private async disableCircuitBreaker(componentName: string, reason: string): Promise<void> {
// Implementierung für Circuit Breaker-Deaktivierung
}
private enableEnhancedMetrics(duration: number): void {
// Implementierung für erweiterte Metriken
}
private async notifyOperationsTeam(notification: any): Promise<void> {
// Implementierung für Team-Benachrichtigungen
}
}Diese umfassende Betrachtung von Debugging und Troubleshooting zeigt, dass erfolgreiches Problem-Solving in NestJS-Anwendungen eine Kombination aus systematischen Strategien, den richtigen Werkzeugen und einem tiefen Verständnis der zugrunde liegenden Systeme erfordert. Wie ein erfahrener Detektiv sammeln Sie Hinweise, analysieren Muster und ziehen logische Schlüsse, um auch die schwierigsten Probleme zu lösen.
Von strategischem Debugging über Performance Profiling bis hin zu Memory Leak Detection und Production Debugging - jede Technik hat ihren Platz in Ihrem Debugging-Arsenal. Database Query Analysis hilft Ihnen dabei, eine der häufigsten Ursachen für Performance-Probleme zu identifizieren, während Distributed Tracing in komplexen Mikroservice-Umgebungen unerlässlich ist.
Der Schlüssel liegt darin, die richtige Technik für das jeweilige Problem zu wählen und dabei immer die Auswirkungen auf das laufende System zu berücksichtigen. Denken Sie daran: Debugging ist nicht nur das Beheben von Problemen, sondern auch das Verstehen Ihrer Anwendung auf einer tieferen Ebene. Jedes gelöste Problem macht Sie zu einem besseren Entwickler und trägt zur langfristigen Stabilität und Performance Ihrer Anwendung bei.
In der Produktion gilt das Prinzip “Do no harm” - Ihre Debugging-Bemühungen dürfen niemals das System weiter destabilisieren. Mit den hier vorgestellten sicheren Debugging-Strategien und Werkzeugen können Sie auch die komplexesten Produktionsprobleme effektiv angehen, ohne dabei Kompromisse bei der Stabilität oder Sicherheit einzugehen.