UNPKG

traffic-monitor-mqtt

Version:

Zentrales Traffic Monitoring via MQTT für Node.js Anwendungen

470 lines (469 loc) 19.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MongoDBMonitor = void 0; const mongodb_1 = require("mongodb"); const debug_1 = __importDefault(require("debug")); const log = (0, debug_1.default)('traffic-monitor:mongodb'); // Cache für häufig verwendete Methoden const TRACKED_METHODS = new Set([ 'find', 'findOne', 'insertOne', 'insertMany', 'updateOne', 'updateMany', 'deleteOne', 'deleteMany', 'aggregate', 'distinct', 'bulkWrite', 'watch' ]); // Methoden, die synchron ein Cursor oder ChangeStream zurückgeben const SYNC_METHODS = new Set([ 'find', 'aggregate', 'watch' ]); // Methoden, die ein Promise zurückgeben const ASYNC_METHODS = new Set([ 'insertOne', 'insertMany', 'updateOne', 'updateMany', 'deleteOne', 'deleteMany', 'distinct', 'bulkWrite' ]); const TRACKED_CURSOR_METHODS = new Set([ 'next', 'hasNext', 'forEach', 'map', 'close', 'toArray' ]); // Hilfsfunktionen für Typ-Checks const isCursorLike = (obj) => { return obj && typeof obj === 'object' && ( // Entweder ein echter MongoDB-Cursor obj instanceof mongodb_1.FindCursor || obj instanceof mongodb_1.AggregationCursor || // Oder ein Cursor-ähnliches Objekt mit den wichtigsten Methoden (typeof obj.toArray === 'function' && typeof obj.next === 'function' && typeof obj.hasNext === 'function')); }; const isChangeStreamLike = (obj) => { return obj && typeof obj === 'object' && ( // Entweder ein echter ChangeStream obj instanceof mongodb_1.ChangeStream || // Oder ein ChangeStream-ähnliches Objekt (typeof obj.on === 'function' && typeof obj.emit === 'function' && typeof obj.close === 'function')); }; // Optimierte Payload-Größenberechnung function calculatePayloadSize(data) { try { if (!data) return 0; if (Array.isArray(data)) { return data.reduce((size, item) => size + calculatePayloadSize(item), 0); } if (typeof data === 'object') { // Schnellere Alternative zu JSON.stringify für große Objekte return Object.entries(data).reduce((size, [key, value]) => { return size + key.length + calculatePayloadSize(value); }, 0); } if (typeof data === 'string') return data.length; if (typeof data === 'number') return 8; // Durchschnittliche Größe einer Zahl if (typeof data === 'boolean') return 1; return 0; } catch (error) { log('Error calculating payload size:', error); return 0; } } // Optimierte Bulk-Operation-Analyse mit Memoization const operationAnalysisCache = new WeakMap(); function analyzeBulkOperations(operations) { // Prüfe Cache const cached = operationAnalysisCache.get(operations); if (cached) return cached; const operationCounts = new Map(); operations.forEach(operation => { const operationType = Object.keys(operation)[0]; operationCounts.set(operationType, (operationCounts.get(operationType) || 0) + 1); }); const result = Array.from(operationCounts.entries()).map(([type, count]) => ({ type, count })); // Speichere im Cache operationAnalysisCache.set(operations, result); return result; } class MongoDBMonitor { constructor(eventBatcher, clientConfig) { this.eventBatcher = eventBatcher; this.clientConfig = clientConfig; this.methodCache = new WeakMap(); this.eventDetailsCache = new Map(); } // Cache für Proxy-Handler-Methoden getCachedMethod(target, prop, originalMethod) { let targetCache = this.methodCache.get(target); if (!targetCache) { targetCache = new Map(); this.methodCache.set(target, targetCache); } let cachedMethod = targetCache.get(prop); if (!cachedMethod) { cachedMethod = originalMethod; targetCache.set(prop, cachedMethod); } return cachedMethod; } wrapMongoClient(client) { if (!client) { throw new Error('MongoClient is required'); } if (typeof client.db !== 'function') { throw new Error('Invalid MongoClient: Must have a db method'); } try { const originalDb = client.db.bind(client); const self = this; client.db = function (dbName, options) { try { const db = originalDb(dbName, options); return self.wrapDb(db); } catch (err) { log('Error in db method:', err); return originalDb(dbName, options); } }; return client; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; log('Error wrapping MongoClient:', errorMessage); throw new Error(`Failed to wrap MongoClient: ${errorMessage}`); } } wrapDb(db) { const originalCollection = db.collection.bind(db); const self = this; db.collection = function (name, options) { const collection = originalCollection(name, options); return self.wrapCollection(collection, db.databaseName); }; return db; } createTrafficEvent(dbName, collectionName, operation, startTime, success, error = null, payloadSize = 0, additionalDetails = {}) { // Cache-Key für häufig verwendete Event-Details const cacheKey = `${dbName}:${collectionName}:${operation}`; let cachedDetails = this.eventDetailsCache.get(cacheKey); if (!cachedDetails) { // Spezielle Behandlung für Watch-Events const operationType = operation.startsWith('watch:') ? `db:${operation}` // z.B. "db:watch:start" : `db:${operation}`; // z.B. "db:insertOne" cachedDetails = { clientInfo: { name: this.clientConfig.name, environment: this.clientConfig.environment, hostname: this.clientConfig.hostname }, operationType: operationType, targetSystem: `mongodb:${dbName}`, details: { collection: collectionName, operation } }; this.eventDetailsCache.set(cacheKey, cachedDetails); } return { ...cachedDetails, timestamp: new Date().toISOString(), performance: { executionTimeMs: Date.now() - startTime, payloadSizeBytes: payloadSize }, details: { ...cachedDetails.details, success, error, ...additionalDetails } }; } calculatePayloadSize(args) { try { return JSON.stringify(args).length; } catch (error) { log('Error calculating payload size:', error); return 0; } } getChangeStreamEventDetails(change) { const baseDetails = { operationType: change.operationType, streamStatus: 'active' }; switch (change.operationType) { case 'insert': return { ...baseDetails, documentKey: change.documentKey, fullDocument: change.fullDocument }; case 'update': const updateChange = change; return { ...baseDetails, documentKey: updateChange.documentKey, updateDescription: updateChange.updateDescription, ...(updateChange.fullDocument && { fullDocument: updateChange.fullDocument }) }; case 'delete': return { ...baseDetails, documentKey: change.documentKey }; case 'drop': return { ...baseDetails, ns: change.ns }; case 'rename': const renameChange = change; return { ...baseDetails, ns: renameChange.ns, to: renameChange.to }; case 'dropDatabase': return { ...baseDetails, ns: change.ns }; case 'invalidate': return { ...baseDetails, _id: change._id }; default: return baseDetails; } } wrapCollection(collection, dbName) { const self = this; const collectionName = collection.collectionName; return new Proxy(collection, { get(target, prop, receiver) { const originalMethod = Reflect.get(target, prop, receiver); if (typeof originalMethod !== 'function') { return originalMethod; } if (!TRACKED_METHODS.has(prop.toString())) { return originalMethod; } // Generische Wrapper-Funktion return function (...args) { const startTime = Date.now(); const payloadSize = calculatePayloadSize(args); const operationName = prop.toString(); // Hilfsfunktion zum Erstellen und Senden des Events const createAndSendEvent = async (success, error = null, result = null) => { const event = self.createTrafficEvent(dbName, collectionName, operationName, startTime, success, error, payloadSize, success ? self.getOperationDetails(operationName, args, result) : {}); try { await self.eventBatcher.addEvent(event); } catch (err) { log('Error sending event:', err); } }; try { // Synchrone Methode (gibt Cursor oder ChangeStream zurück) if (SYNC_METHODS.has(operationName)) { const result = originalMethod.apply(target, args); // Event senden createAndSendEvent(true, null, result); // Cursor oder ChangeStream wrappen if (isCursorLike(result)) { return self.wrapCursor(result, dbName, collectionName, operationName); } if (isChangeStreamLike(result)) { return self.wrapChangeStream(result, dbName, collectionName); } return result; } // Asynchrone Methode (gibt Promise zurück) const result = originalMethod.apply(target, args); // Prüfe, ob das Ergebnis ein Promise ist if (result && typeof result.then === 'function') { return result .then(async (asyncResult) => { await createAndSendEvent(true, null, asyncResult); return asyncResult; }) .catch(async (error) => { await createAndSendEvent(false, error.message); throw error; }); } // Synchrones Ergebnis createAndSendEvent(true, null, result); return result; } catch (error) { createAndSendEvent(false, error.message); throw error; } }; } }); } wrapCursor(cursor, dbName, collectionName, operation) { const self = this; return new Proxy(cursor, { get(target, prop, receiver) { const originalMethod = Reflect.get(target, prop, receiver); if (typeof originalMethod !== 'function' || !TRACKED_CURSOR_METHODS.has(prop.toString())) { return originalMethod; } // Cursor-Methoden sind immer asynchron return function (...args) { const cursorStartTime = Date.now(); const payloadSize = calculatePayloadSize(args); const cursorOperation = `${operation}:${prop.toString()}`; // Hilfsfunktion zum Erstellen und Senden des Events const createAndSendEvent = async (success, error = null) => { const event = self.createTrafficEvent(dbName, collectionName, cursorOperation, cursorStartTime, success, error, payloadSize); try { await self.eventBatcher.addEvent(event); } catch (err) { log('Error sending event:', err); } }; try { const result = originalMethod.apply(target, args); // Cursor-Methoden geben immer ein Promise zurück if (result && typeof result.then === 'function') { // Event sofort senden, da die Operation gestartet wurde createAndSendEvent(true); return result .then(async (asyncResult) => { return asyncResult; }) .catch(async (error) => { await createAndSendEvent(false, error.message); throw error; }); } // Für den unwahrscheinlichen Fall einer synchronen Cursor-Methode createAndSendEvent(true); return result; } catch (error) { createAndSendEvent(false, error.message); throw error; } }; } }); } wrapChangeStream(changeStream, dbName, collectionName) { const self = this; const startTime = Date.now(); // Hilfsfunktion zum Erstellen und Senden des Events const createAndSendEvent = async (operation, success, error = null, additionalDetails = {}) => { const event = self.createTrafficEvent(dbName, collectionName, operation, startTime, success, error, 0, additionalDetails); try { await self.eventBatcher.addEvent(event); } catch (err) { log('Error sending event:', err); } }; // Sende Start-Event synchron createAndSendEvent('watch:start', true); // Wrappen der close-Methode const originalClose = changeStream.close?.bind(changeStream); if (originalClose) { changeStream.close = function () { try { const result = originalClose(); if (result && typeof result.then === 'function') { return result .then(async (closeResult) => { await createAndSendEvent('watch:close', true); return closeResult; }) .catch(async (error) => { await createAndSendEvent('watch:close', false, error.message); throw error; }); } createAndSendEvent('watch:close', true); return result; } catch (error) { createAndSendEvent('watch:close', false, error.message); throw error; } }; } // Wrappen der on-Methode für Change-Events const originalOn = changeStream.on?.bind(changeStream); if (originalOn) { changeStream.on = function (event, listener) { if (event === 'change') { return originalOn(event, async (change) => { await createAndSendEvent('watch:change', true, null, self.getChangeStreamEventDetails(change)); listener(change); }); } return originalOn(event, listener); }; } // Wrappen der emit-Methode const originalEmit = changeStream.emit?.bind(changeStream); if (originalEmit) { changeStream.emit = function (event, ...args) { if (event === 'change') { const change = args[0]; // Synchron senden für emit createAndSendEvent('watch:change', true, null, self.getChangeStreamEventDetails(change)); } return originalEmit(event, ...args); }; } return changeStream; } getOperationDetails(operation, args, result) { switch (operation) { case 'aggregate': return { pipelineStages: args[0]?.map((stage) => Object.keys(stage)[0]) || [], pipelineLength: args[0]?.length || 0 }; case 'bulkWrite': return { operationDetails: analyzeBulkOperations(args[0] || []) }; case 'insertOne': return { insertedId: result?.insertedId }; case 'updateOne': case 'updateMany': return { matchedCount: result?.matchedCount, modifiedCount: result?.modifiedCount, upsertedId: result?.upsertedId }; case 'deleteOne': case 'deleteMany': return { deletedCount: result?.deletedCount }; default: return {}; } } } exports.MongoDBMonitor = MongoDBMonitor;