UNPKG

alfred-logger-sdk

Version:

Production-ready data collection SDK for feeding structured events to LLM Data Agents with auto-capture capabilities

278 lines (234 loc) 8.01 kB
const http = require('http'); const https = require('https'); const url = require('url'); class AutoCapture { constructor(logger, config = {}) { this.logger = logger; this.config = { enabled: config.enabled !== false, captureRequestBody: config.captureRequestBody !== false, captureResponseBody: config.captureResponseBody !== false, maxBodySize: config.maxBodySize || 64 * 1024, // 64KB default sampleRate: Math.min(Math.max(config.sampleRate || 1.0, 0), 1), // 0-1 range ignoredPaths: config.ignoredPaths || ['/health', '/ping', '/favicon.ico'], ignoredHosts: config.ignoredHosts || [], captureHeaders: config.captureHeaders !== false, sensitiveHeaders: config.sensitiveHeaders || [ 'authorization', 'cookie', 'set-cookie', 'x-api-key', 'x-auth-token' ], ...config }; this.originalHttpRequest = null; this.originalHttpsRequest = null; this.isPatched = false; } start() { if (!this.config.enabled || this.isPatched) { return; } this.patchHttpModules(); this.isPatched = true; } stop() { if (!this.isPatched) { return; } this.unpatchHttpModules(); this.isPatched = false; } patchHttpModules() { // Store original methods this.originalHttpRequest = http.request; this.originalHttpsRequest = https.request; // Patch HTTP http.request = this.createPatchedRequest(http, this.originalHttpRequest); // Patch HTTPS https.request = this.createPatchedRequest(https, this.originalHttpsRequest); } unpatchHttpModules() { if (this.originalHttpRequest) { http.request = this.originalHttpRequest; this.originalHttpRequest = null; } if (this.originalHttpsRequest) { https.request = this.originalHttpsRequest; this.originalHttpsRequest = null; } } createPatchedRequest(httpModule, originalRequest) { return (...args) => { try { const req = originalRequest.apply(httpModule, args); this.instrumentRequest(req, args); return req; } catch (error) { console.error('Error in auto-capture request patch:', error.message); return originalRequest.apply(httpModule, args); } }; } instrumentRequest(req, requestArgs) { const startTime = Date.now(); let requestUrl; let requestOptions = {}; // Parse request arguments to extract URL and options if (typeof requestArgs[0] === 'string') { requestUrl = requestArgs[0]; requestOptions = requestArgs[1] || {}; } else if (typeof requestArgs[0] === 'object') { requestOptions = requestArgs[0]; requestUrl = url.format(requestOptions); } // Check if this request should be ignored if (this.shouldIgnoreRequest(requestUrl, requestOptions)) { return; } // Apply sampling if (Math.random() > this.config.sampleRate) { return; } const traceId = this.logger.getCurrentTraceId() || this.logger.generateTraceId(); let requestBody = ''; let responseBody = ''; // Capture request body if enabled if (this.config.captureRequestBody) { const originalWrite = req.write; const originalEnd = req.end; req.write = function(chunk, encoding) { if (chunk && requestBody.length < this.config.maxBodySize) { requestBody += chunk.toString(); } return originalWrite.call(this, chunk, encoding); }.bind(this); req.end = function(chunk, encoding) { if (chunk && requestBody.length < this.config.maxBodySize) { requestBody += chunk.toString(); } return originalEnd.call(this, chunk, encoding); }.bind(this); } // Log request const requestData = { url: requestUrl, method: requestOptions.method || 'GET', headers: this.sanitizeHeaders(requestOptions.headers || {}), userAgent: (requestOptions.headers || {})['user-agent'], contentType: (requestOptions.headers || {})['content-type'] }; if (this.config.captureRequestBody && requestBody) { requestData.body = this.truncateBody(requestBody); } this.logger.collectEvent('http_request', requestData, { traceId }); // Handle response req.on('response', (res) => { try { this.instrumentResponse(res, requestUrl, traceId, startTime, responseBody); } catch (error) { console.error('Error instrumenting response:', error.message); } }); // Handle request errors req.on('error', (error) => { try { const duration = Date.now() - startTime; this.logger.collectEvent('http_response', { url: requestUrl, method: requestOptions.method || 'GET', status: 0, error: error.message, duration }, { traceId }); } catch (logError) { console.error('Error logging request error:', logError.message); } }); } instrumentResponse(res, requestUrl, traceId, startTime, responseBody) { const duration = Date.now() - startTime; // Capture response body if enabled if (this.config.captureResponseBody) { const originalOn = res.on; res.on = function(event, listener) { if (event === 'data') { const originalListener = listener; const wrappedListener = (chunk) => { if (chunk && responseBody.length < this.config.maxBodySize) { responseBody += chunk.toString(); } return originalListener.call(this, chunk); }; return originalOn.call(this, event, wrappedListener.bind(this)); } return originalOn.call(this, event, listener); }.bind(this); } // Log response when complete res.on('end', () => { try { const responseData = { url: requestUrl, status: res.statusCode, statusText: res.statusMessage, headers: this.sanitizeHeaders(res.headers || {}), contentType: res.headers['content-type'], contentLength: parseInt(res.headers['content-length']) || 0, duration }; if (this.config.captureResponseBody && responseBody) { responseData.body = this.truncateBody(responseBody); } this.logger.collectEvent('http_response', responseData, { traceId }); } catch (error) { console.error('Error logging response:', error.message); } }); } shouldIgnoreRequest(requestUrl, options) { if (!requestUrl) return true; try { const parsedUrl = new URL(requestUrl); // Check ignored hosts if (this.config.ignoredHosts.includes(parsedUrl.hostname)) { return true; } // Check ignored paths if (this.config.ignoredPaths.some(path => parsedUrl.pathname.startsWith(path))) { return true; } // Ignore requests to the logger's own endpoint if (this.logger.config.endpoint && requestUrl.startsWith(this.logger.config.endpoint)) { return true; } return false; } catch (error) { // If URL parsing fails, don't ignore return false; } } sanitizeHeaders(headers) { if (!this.config.captureHeaders) { return {}; } const sanitized = {}; for (const [key, value] of Object.entries(headers)) { const lowerKey = key.toLowerCase(); if (this.config.sensitiveHeaders.includes(lowerKey)) { sanitized[key] = '[REDACTED]'; } else { sanitized[key] = value; } } return sanitized; } truncateBody(body) { if (!body) return ''; if (typeof body === 'string' && body.length > this.config.maxBodySize) { return body.substring(0, this.config.maxBodySize) + '... (truncated)'; } return body; } updateConfig(newConfig) { Object.assign(this.config, newConfig); } } module.exports = AutoCapture;