meshed-monitor-sdk
Version:
JavaScript/Node.js SDK for MeshedMonitor with real-time logging and advanced features
616 lines (509 loc) • 15.8 kB
JavaScript
/**
* 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;