traffic-monitor-mqtt
Version:
Zentrales Traffic Monitoring via MQTT für Node.js Anwendungen
470 lines (469 loc) • 19.2 kB
JavaScript
;
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;