Skip to main content
Glama
structured-logger.js6.5 kB
/** * Structured JSON Logger for machine-parseable logs * * Provides structured logging with JSON output for observability tools * (CloudWatch, Datadog, Splunk, etc.) */ class StructuredLogger { /** * Create a structured logger instance * @param {Object} options - Logger configuration * @param {string} options.level - Minimum log level (debug, info, warn, error) * @param {Object} options.context - Default context to include in all log entries * @param {string} options.correlationId - Correlation ID for tracking related operations */ constructor(options = {}) { this.level = options.level || 'info'; this.context = options.context || {}; this.correlationId = options.correlationId || this.generateCorrelationId(); this.levels = { debug: 0, info: 1, warn: 2, error: 3 }; this.timers = new Map(); // Track active timers } /** * Generate a unique correlation ID for tracking related operations * Format: timestamp-random (e.g., "1699564800000-abc123xyz") * @returns {string} Unique correlation ID */ generateCorrelationId() { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Core logging method - outputs single-line JSON * @param {string} level - Log level * @param {string} message - Log message * @param {Object} metadata - Additional metadata to include */ log(level, message, metadata = {}) { // Check if this level should be logged if (this.levels[level] < this.levels[this.level]) { return; } const entry = { timestamp: new Date().toISOString(), level, message, correlation_id: this.correlationId, ...this.context, ...metadata }; console.log(JSON.stringify(entry)); } /** * Create a child logger that shares the same correlation ID * Useful for tracking related operations across different contexts * @param {Object} context - Additional context for the child logger * @returns {StructuredLogger} New logger instance with shared correlation ID */ createChild(context = {}) { return new StructuredLogger({ level: this.level, context: { ...this.context, ...context }, correlationId: this.correlationId }); } /** * Log debug message * @param {string} message - Log message * @param {Object} metadata - Additional metadata */ debug(message, metadata) { this.log('debug', message, metadata); } /** * Log info message * @param {string} message - Log message * @param {Object} metadata - Additional metadata */ info(message, metadata) { this.log('info', message, metadata); } /** * Log warning message * @param {string} message - Log message * @param {Object} metadata - Additional metadata */ warn(message, metadata) { this.log('warn', message, metadata); } /** * Log error message * @param {string} message - Log message * @param {Object} metadata - Additional metadata */ error(message, metadata) { this.log('error', message, metadata); } /** * Sanitize HTTP headers by removing sensitive authentication data * @param {Object} headers - HTTP headers object * @returns {Object} Sanitized headers with secrets removed/redacted */ sanitizeHeaders(headers) { if (!headers || typeof headers !== 'object') { return {}; } const safe = { ...headers }; // Delete known sensitive headers delete safe['Authorization']; delete safe['X-Api-Key']; delete safe['X-Auth-Token']; // Check for auth-related keys case-insensitively Object.keys(safe).forEach(key => { const lowerKey = key.toLowerCase(); if (lowerKey.includes('auth') || lowerKey.includes('token') || lowerKey.includes('key') || lowerKey.includes('secret') || lowerKey.includes('password')) { safe[key] = '[REDACTED]'; } }); return safe; } /** * Log an API request with sanitized headers * @param {string} method - HTTP method (GET, POST, etc.) * @param {string} url - Request URL * @param {Object} options - Request options * @param {Object} options.headers - Request headers * @param {string|Object} options.body - Request body */ logAPIRequest(method, url, options = {}) { this.debug('API request', { method, url, headers: this.sanitizeHeaders(options.headers || {}), bodyLength: options.body ? (typeof options.body === 'string' ? options.body.length : JSON.stringify(options.body).length) : 0 }); } /** * Log an API response with status and timing * @param {string} url - Request URL * @param {number} status - HTTP status code * @param {number} duration - Request duration in milliseconds */ logAPIResponse(url, status, duration) { const level = status >= 400 ? 'error' : 'info'; this.log(level, 'API response', { url, status, duration_ms: duration }); } /** * Start a named timer for performance tracking * @param {string} name - Timer name * @returns {string} Timer name (for method chaining) */ startTimer(name) { this.timers.set(name, Date.now()); this.debug('Timer started', { timer: name }); return name; } /** * End a named timer and log the duration * @param {string} name - Timer name * @param {Object} metadata - Additional metadata to include in log * @returns {number|null} Duration in milliseconds, or null if timer not found */ endTimer(name, metadata = {}) { const startTime = this.timers.get(name); if (!startTime) { this.warn('Timer not found', { timer: name }); return null; } const duration = Date.now() - startTime; this.timers.delete(name); this.info('Timer completed', { timer: name, duration_ms: duration, ...metadata }); return duration; } /** * Wrap an async operation with automatic timing * @param {string} name - Timer name * @param {Function} operation - Async function to execute * @returns {Promise<any>} Result of the operation */ async timed(name, operation) { this.startTimer(name); try { const result = await operation(); this.endTimer(name, { status: 'success' }); return result; } catch (error) { this.endTimer(name, { status: 'error', error: error.message }); throw error; } } } module.exports = { StructuredLogger };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/JaxonDigital/optimizely-dxp-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server