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
JavaScript
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;