UNPKG

meshed-monitor-sdk

Version:

JavaScript/Node.js SDK for MeshedMonitor with real-time logging and advanced features

616 lines (509 loc) 15.8 kB
/** * MeshedMonitor JavaScript SDK * Enhanced version with real-time logging, batch processing, and advanced features */ class MeshedMonitorSDK { constructor(config = {}) { this.config = { apiKey: config.apiKey || process.env.MESHED_MONITOR_API_KEY, apiUrl: config.apiUrl || process.env.MESHED_MONITOR_API_URL || 'http://localhost:3001/api', projectId: config.projectId, environment: config.environment || process.env.NODE_ENV || 'development', debug: config.debug || false, batchSize: config.batchSize || 50, batchTimeout: config.batchTimeout || 5000, // 5 seconds maxRetries: config.maxRetries || 3, retryDelay: config.retryDelay || 1000, enableBackgroundSync: config.enableBackgroundSync !== false, ...config }; if (!this.config.apiKey) { console.warn('MeshedMonitor: No API key provided. Logging will be disabled.'); this.disabled = true; return; } this.disabled = false; this.context = { user: null, session: this._generateSessionId(), tags: {}, release: null }; // Batch processing this.logBuffer = []; this.errorBuffer = []; this.batchTimer = null; // Request correlation this.currentRequestId = null; this.currentTraceId = null; // Performance tracking this.transactions = new Map(); this.spans = new Map(); // Background sync queue for offline support this.syncQueue = []; this.isSyncing = false; this._setupPeriodicFlush(); this._setupBeforeUnloadHandler(); if (this.config.debug) { console.log('MeshedMonitor SDK initialized', this.config); } } // ========== CONFIGURATION & CONTEXT ========== setUser(user) { this.context.user = { id: user.id, email: user.email, name: user.name, ...user }; return this; } setTag(key, value) { this.context.tags[key] = value; return this; } setTags(tags) { this.context.tags = { ...this.context.tags, ...tags }; return this; } setRelease(version) { this.context.release = version; return this; } setRequestContext(requestId, traceId) { this.currentRequestId = requestId; this.currentTraceId = traceId; return this; } clearContext() { this.context = { user: null, session: this._generateSessionId(), tags: {}, release: null }; this.currentRequestId = null; this.currentTraceId = null; return this; } // ========== LOGGING METHODS ========== log(message, level = 'INFO', metadata = {}) { if (this.disabled) return this; const logEntry = this._createLogEntry(message, level, metadata); if (this.config.enableBackgroundSync) { this._addToBuffer(logEntry); } else { this._sendLogImmediate(logEntry); } return this; } debug(message, metadata = {}) { return this.log(message, 'DEBUG', metadata); } info(message, metadata = {}) { return this.log(message, 'INFO', metadata); } warn(message, metadata = {}) { return this.log(message, 'WARN', metadata); } error(message, metadata = {}) { return this.log(message, 'ERROR', metadata); } // Enhanced error capturing with stack traces and context captureError(error, additionalData = {}) { if (this.disabled) return this; const errorData = { message: error.message || String(error), stack: error.stack || new Error().stack, level: 'ERROR', userId: this.context.user?.id, userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined, url: typeof window !== 'undefined' ? window.location?.href : undefined, environment: this.config.environment, release: this.context.release, tags: { ...this.context.tags }, metadata: { ...additionalData, errorType: error.constructor.name, timestamp: new Date().toISOString() }, sessionId: this.context.session, requestId: this.currentRequestId, traceId: this.currentTraceId }; // Send errors immediately for better alerting this._sendErrorImmediate(errorData); return this; } // ========== PERFORMANCE TRACKING ========== startTransaction(name, operation = 'custom') { const transactionId = this._generateId(); const transaction = { id: transactionId, name, operation, startTime: Date.now(), spans: [], tags: { ...this.context.tags } }; this.transactions.set(transactionId, transaction); return { id: transactionId, startSpan: (spanName) => this.startSpan(spanName, transactionId), setTag: (key, value) => { transaction.tags[key] = value; return this; }, finish: () => this.finishTransaction(transactionId) }; } startSpan(name, transactionId, operation = 'custom') { const spanId = this._generateId(); const span = { id: spanId, name, operation, transactionId, startTime: Date.now(), tags: {} }; this.spans.set(spanId, span); if (transactionId && this.transactions.has(transactionId)) { this.transactions.get(transactionId).spans.push(spanId); } return { id: spanId, setTag: (key, value) => { span.tags[key] = value; return this; }, finish: () => this.finishSpan(spanId) }; } finishSpan(spanId) { const span = this.spans.get(spanId); if (!span) return; span.duration = Date.now() - span.startTime; this.log(`Span completed: ${span.name}`, 'DEBUG', { spanId, transactionId: span.transactionId, operation: span.operation, duration: span.duration, tags: span.tags }); this.spans.delete(spanId); } finishTransaction(transactionId) { const transaction = this.transactions.get(transactionId); if (!transaction) return; transaction.duration = Date.now() - transaction.startTime; // Finish all remaining spans transaction.spans.forEach(spanId => this.finishSpan(spanId)); this.log(`Transaction completed: ${transaction.name}`, 'INFO', { transactionId, operation: transaction.operation, duration: transaction.duration, spanCount: transaction.spans.length, tags: transaction.tags }); this.transactions.delete(transactionId); } // ========== BATCH PROCESSING ========== _createLogEntry(message, level, metadata) { return { level, message, timestamp: new Date().toISOString(), userId: this.context.user?.id, sessionId: this.context.session, requestId: this.currentRequestId, source: metadata.source || 'sdk-js', environment: this.config.environment, release: this.context.release, tags: { ...this.context.tags, ...metadata.tags }, metadata: { ...metadata, userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined, url: typeof window !== 'undefined' ? window.location?.href : undefined, sdkVersion: '1.0.0' }, traceId: this.currentTraceId, spanId: metadata.spanId, duration: metadata.duration }; } _addToBuffer(logEntry) { this.logBuffer.push(logEntry); if (this.logBuffer.length >= this.config.batchSize) { this._flushLogs(); } else if (!this.batchTimer) { this.batchTimer = setTimeout(() => { this._flushLogs(); }, this.config.batchTimeout); } } _flushLogs() { if (this.logBuffer.length === 0) return; const logsToSend = [...this.logBuffer]; this.logBuffer = []; if (this.batchTimer) { clearTimeout(this.batchTimer); this.batchTimer = null; } this._sendBatchLogs(logsToSend); } // ========== NETWORK LAYER ========== async _sendBatchLogs(logs) { try { const response = await this._makeRequest('/logs/stream/batch', { method: 'POST', body: JSON.stringify({ logs }), headers: { 'Content-Type': 'application/json' } }); if (this.config.debug) { console.log(`MeshedMonitor: Sent ${logs.length} logs successfully`); } } catch (error) { console.error('MeshedMonitor: Failed to send batch logs:', error); // Add failed logs back to sync queue for retry if (this.config.enableBackgroundSync) { this.syncQueue.push(...logs.map(log => ({ type: 'log', data: log }))); this._scheduleSync(); } } } async _sendLogImmediate(log) { try { await this._makeRequest('/logs/stream', { method: 'POST', body: JSON.stringify(log), headers: { 'Content-Type': 'application/json' } }); } catch (error) { console.error('MeshedMonitor: Failed to send log:', error); if (this.config.enableBackgroundSync) { this.syncQueue.push({ type: 'log', data: log }); this._scheduleSync(); } } } async _sendErrorImmediate(errorData) { try { await this._makeRequest('/sdk/errors', { method: 'POST', body: JSON.stringify(errorData), headers: { 'Content-Type': 'application/json' } }); if (this.config.debug) { console.log('MeshedMonitor: Error sent successfully'); } } catch (error) { console.error('MeshedMonitor: Failed to send error:', error); if (this.config.enableBackgroundSync) { this.syncQueue.push({ type: 'error', data: errorData }); this._scheduleSync(); } } } async _makeRequest(endpoint, options = {}) { const url = `${this.config.apiUrl}${endpoint}`; const headers = { 'Authorization': `Bearer ${this.config.apiKey}`, 'User-Agent': 'MeshedMonitor-SDK-JS/1.0.0', ...options.headers }; let lastError; for (let attempt = 0; attempt < this.config.maxRetries; attempt++) { try { const response = await fetch(url, { ...options, headers }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { lastError = error; if (attempt < this.config.maxRetries - 1) { const delay = this.config.retryDelay * Math.pow(2, attempt); await new Promise(resolve => setTimeout(resolve, delay)); } } } throw lastError; } // ========== BACKGROUND SYNC ========== _scheduleSync() { if (this.isSyncing || this.syncQueue.length === 0) return; setTimeout(() => { this._performSync(); }, 1000); // Wait 1 second before syncing } async _performSync() { if (this.isSyncing || this.syncQueue.length === 0) return; this.isSyncing = true; const itemsToSync = [...this.syncQueue]; try { // Group logs and errors separately const logs = itemsToSync.filter(item => item.type === 'log').map(item => item.data); const errors = itemsToSync.filter(item => item.type === 'error').map(item => item.data); // Send in batches if (logs.length > 0) { await this._sendBatchLogs(logs); } if (errors.length > 0) { for (const error of errors) { await this._sendErrorImmediate(error); } } // Clear sync queue on success this.syncQueue = []; if (this.config.debug) { console.log(`MeshedMonitor: Background sync completed (${itemsToSync.length} items)`); } } catch (error) { console.error('MeshedMonitor: Background sync failed:', error); // Keep failed items in queue for next attempt const failedItems = this.syncQueue.splice(0, itemsToSync.length); this.syncQueue.push(...failedItems); } finally { this.isSyncing = false; } } // ========== LIFECYCLE MANAGEMENT ========== _setupPeriodicFlush() { if (typeof setInterval !== 'undefined') { this.flushInterval = setInterval(() => { this._flushLogs(); }, this.config.batchTimeout); } } _setupBeforeUnloadHandler() { if (typeof window !== 'undefined' && window.addEventListener) { window.addEventListener('beforeunload', () => { this.destroy(); }); } if (typeof process !== 'undefined' && process.on) { process.on('SIGINT', () => { this.destroy(); process.exit(0); }); process.on('SIGTERM', () => { this.destroy(); process.exit(0); }); } } // Force flush all pending data async flush() { this._flushLogs(); if (this.syncQueue.length > 0) { await this._performSync(); } } // Clean up resources destroy() { if (this.flushInterval) { clearInterval(this.flushInterval); } if (this.batchTimer) { clearTimeout(this.batchTimer); } // Flush any remaining logs synchronously this._flushLogs(); if (this.config.debug) { console.log('MeshedMonitor SDK destroyed'); } } // ========== UTILITY METHODS ========== _generateId() { return Math.random().toString(36).substring(2) + Date.now().toString(36); } _generateSessionId() { return `session_${this._generateId()}`; } // ========== PUBLIC UTILITY METHODS ========== // Middleware for Express.js expressMiddleware() { return (req, res, next) => { const requestId = req.headers['x-request-id'] || this._generateId(); const traceId = req.headers['x-trace-id'] || this._generateId(); req.meshedMonitor = { requestId, traceId, log: (message, level = 'INFO', metadata = {}) => { this.setRequestContext(requestId, traceId); this.log(message, level, { ...metadata, method: req.method, url: req.url, userAgent: req.headers['user-agent'], ip: req.ip }); }, startTransaction: (name) => { this.setRequestContext(requestId, traceId); return this.startTransaction(name, 'http'); } }; // Log request this.setRequestContext(requestId, traceId); this.info(`${req.method} ${req.url}`, { method: req.method, url: req.url, userAgent: req.headers['user-agent'], ip: req.ip }); // Override res.json to log responses const originalJson = res.json; res.json = function(data) { req.meshedMonitor.log(`Response sent: ${res.statusCode}`, 'INFO', { statusCode: res.statusCode, responseSize: JSON.stringify(data).length }); return originalJson.call(this, data); }; next(); }; } // Wrapper for async functions wrap(fn, name = fn.name || 'anonymous') { return async (...args) => { const transaction = this.startTransaction(`function_${name}`); try { const result = await fn(...args); return result; } catch (error) { this.captureError(error, { function: name, args: args.length }); throw error; } finally { transaction.finish(); } }; } // Health check helper async healthCheck() { try { await this._makeRequest('/sdk/health', { method: 'GET' }); return true; } catch (error) { return false; } } } // Export for different module systems if (typeof module !== 'undefined' && module.exports) { module.exports = MeshedMonitorSDK; } if (typeof window !== 'undefined') { window.MeshedMonitorSDK = MeshedMonitorSDK; } export default MeshedMonitorSDK;