logs-interceptor-node14
Version:
High-performance, production-ready log interceptor for Node.js 14 applications with Loki integration
546 lines • 19.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseStackTrace = exports.extractErrorMetadata = exports.createCorrelationId = exports.validateConfig = exports.mergeConfigs = exports.loadConfigFromEnv = exports.calculateCompressionRatio = exports.formatBytes = exports.shouldSampleAdvanced = exports.shouldSample = exports.parseLabels = exports.hashSensitiveData = exports.sanitizeData = exports.detectSensitiveData = exports.safeStringify = void 0;
const crypto = __importStar(require("crypto"));
/**
* Safely stringify any value, handling circular references and non-serializable objects
*/
function safeStringify(value, maxDepth = 10) {
const seen = new WeakSet();
let depth = 0;
try {
return JSON.stringify(value, function (key, val) {
// Check depth
if (depth > maxDepth) {
return '[Max Depth Reached]';
}
if (val === null || val === undefined) {
return val;
}
if (typeof val === 'object') {
depth++;
if (seen.has(val)) {
return '[Circular Reference]';
}
seen.add(val);
// Handle special objects
if (val instanceof Buffer) {
return `[Buffer: ${val.length} bytes]`;
}
if (val instanceof Promise) {
return '[Promise]';
}
if (val instanceof WeakMap || val instanceof WeakSet) {
return `[${val.constructor.name}]`;
}
}
if (typeof val === 'function') {
return `[Function: ${val.name || 'anonymous'}]`;
}
if (typeof val === 'symbol') {
return `[Symbol: ${val.toString()}]`;
}
if (typeof val === 'bigint') {
return val.toString() + 'n';
}
if (val instanceof Error) {
return {
name: val.name,
message: val.message,
stack: val.stack,
code: val.code,
};
}
if (val instanceof Date) {
return val.toISOString();
}
if (val instanceof RegExp) {
return val.toString();
}
if (val instanceof Map) {
return {
type: 'Map',
entries: Array.from(val.entries()),
};
}
if (val instanceof Set) {
return {
type: 'Set',
values: Array.from(val.values()),
};
}
return val;
}, 2);
}
catch (error) {
return `[Unserializable: ${error instanceof Error ? error.message : 'Unknown error'}]`;
}
}
exports.safeStringify = safeStringify;
/**
* Detect sensitive data in a string
*/
function detectSensitiveData(text, patterns) {
// Check against patterns
for (const pattern of patterns) {
if (pattern.test(text)) {
return true;
}
}
// Check for common sensitive patterns
const commonPatterns = [
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/,
/\b(?:\d{4}[-\s]?){3}\d{4}\b/,
/\b\d{3}-\d{2}-\d{4}\b/,
/\b\d{3}\.\d{3}\.\d{3}-\d{2}\b/,
/Bearer\s+[A-Za-z0-9\-._~+\/]+=*/i,
/Basic\s+[A-Za-z0-9+\/]+=*/i, // Basic auth
];
for (const pattern of commonPatterns) {
if (pattern.test(text)) {
return true;
}
}
return false;
}
exports.detectSensitiveData = detectSensitiveData;
/**
* Sanitize sensitive data from an object
*/
function sanitizeData(data, sensitivePatterns) {
const sanitized = {};
for (const [key, value] of Object.entries(data)) {
// Check if key matches sensitive patterns
const isKeySensitive = sensitivePatterns.some(pattern => pattern.test(key));
if (isKeySensitive) {
sanitized[key] = '[REDACTED]';
continue;
}
// Handle different value types
if (typeof value === 'string') {
if (detectSensitiveData(value, sensitivePatterns)) {
sanitized[key] = '[REDACTED]';
}
else {
sanitized[key] = value;
}
}
else if (typeof value === 'object' && value !== null) {
if (Array.isArray(value)) {
sanitized[key] = value.map(item => {
if (typeof item === 'string' && detectSensitiveData(item, sensitivePatterns)) {
return '[REDACTED]';
}
if (typeof item === 'object' && item !== null) {
return sanitizeData(item, sensitivePatterns);
}
return item;
});
}
else {
sanitized[key] = sanitizeData(value, sensitivePatterns);
}
}
else {
sanitized[key] = value;
}
}
return sanitized;
}
exports.sanitizeData = sanitizeData;
/**
* Hash sensitive data for tracking without exposing it
*/
function hashSensitiveData(data) {
return crypto
.createHash('sha256')
.update(data)
.digest('hex')
.substring(0, 16);
}
exports.hashSensitiveData = hashSensitiveData;
/**
* Parse labels from environment variable string format
* Format: "key1=value1,key2=value2"
*/
function parseLabels(labelsString) {
const labels = {};
if (!labelsString) {
return labels;
}
try {
// Support JSON format
if (labelsString.startsWith('{')) {
return JSON.parse(labelsString);
}
// Support key=value format
const pairs = labelsString.split(',');
for (const pair of pairs) {
const [key, ...valueParts] = pair.split('=');
if (key && valueParts.length > 0) {
labels[key.trim()] = valueParts.join('=').trim();
}
}
}
catch (error) {
console.warn('Failed to parse labels from environment:', error);
}
return labels;
}
exports.parseLabels = parseLabels;
/**
* Determine if a log should be sampled based on sampling rate
*/
function shouldSample(rate) {
if (rate >= 1.0)
return true;
if (rate <= 0.0)
return false;
return Math.random() < rate;
}
exports.shouldSample = shouldSample;
/**
* Enhanced sampling with support for different strategies
*/
function shouldSampleAdvanced(rate, strategy = 'random', key) {
if (rate >= 1.0)
return true;
if (rate <= 0.0)
return false;
switch (strategy) {
case 'random':
return Math.random() < rate;
case 'deterministic':
// Use a hash of the key to determine sampling
if (!key)
return Math.random() < rate;
const hash = crypto.createHash('md5').update(key).digest();
const hashValue = hash.readUInt32BE(0) / 0xFFFFFFFF;
return hashValue < rate;
case 'adaptive':
// Implement adaptive sampling based on load
// This is a simplified version - in production, you'd track actual load
const cpuUsage = process.cpuUsage();
const loadFactor = Math.min(1, cpuUsage.user / 1000000000); // Normalize to 0-1
const adjustedRate = rate * (1 - loadFactor * 0.5); // Reduce sampling under load
return Math.random() < adjustedRate;
default:
return Math.random() < rate;
}
}
exports.shouldSampleAdvanced = shouldSampleAdvanced;
/**
* Format bytes to human readable string
*/
function formatBytes(bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0)
return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
exports.formatBytes = formatBytes;
/**
* Calculate compression ratio
*/
function calculateCompressionRatio(original, compressed) {
if (original === 0)
return 0;
return Math.round((1 - compressed / original) * 100);
}
exports.calculateCompressionRatio = calculateCompressionRatio;
/**
* Load configuration from environment variables
*/
function loadConfigFromEnv() {
const env = process.env;
const config = {};
// Transport configuration
if (env.LOGS_INTERCEPTOR_URL) {
if (!config.transport)
config.transport = {};
config.transport.url = env.LOGS_INTERCEPTOR_URL;
}
if (env.LOGS_INTERCEPTOR_TENANT_ID) {
if (!config.transport)
config.transport = {};
config.transport.tenantId = env.LOGS_INTERCEPTOR_TENANT_ID;
}
if (env.LOGS_INTERCEPTOR_AUTH_TOKEN) {
if (!config.transport)
config.transport = {};
config.transport.authToken = env.LOGS_INTERCEPTOR_AUTH_TOKEN;
}
// Application metadata
if (env.LOGS_INTERCEPTOR_APP_NAME) {
config.appName = env.LOGS_INTERCEPTOR_APP_NAME;
}
if (env.LOGS_INTERCEPTOR_ENVIRONMENT) {
config.environment = env.LOGS_INTERCEPTOR_ENVIRONMENT;
}
if (env.LOGS_INTERCEPTOR_VERSION) {
config.version = env.LOGS_INTERCEPTOR_VERSION;
}
// Labels
if (env.LOGS_INTERCEPTOR_LABELS) {
config.labels = parseLabels(env.LOGS_INTERCEPTOR_LABELS);
}
// Buffer configuration
if (env.LOGS_INTERCEPTOR_BUFFER_SIZE) {
const bufferSize = parseInt(env.LOGS_INTERCEPTOR_BUFFER_SIZE, 10);
if (!isNaN(bufferSize) && bufferSize > 0) {
config.buffer = {
...config.buffer,
maxSize: bufferSize,
};
}
}
if (env.LOGS_INTERCEPTOR_MAX_MEMORY_MB) {
const maxMemory = parseInt(env.LOGS_INTERCEPTOR_MAX_MEMORY_MB, 10);
if (!isNaN(maxMemory) && maxMemory > 0) {
config.buffer = {
...config.buffer,
maxMemoryMB: maxMemory,
};
}
}
if (env.LOGS_INTERCEPTOR_FLUSH_INTERVAL) {
const flushInterval = parseInt(env.LOGS_INTERCEPTOR_FLUSH_INTERVAL, 10);
if (!isNaN(flushInterval) && flushInterval > 0) {
config.buffer = {
...config.buffer,
flushInterval,
};
}
}
// Filtering
if (env.LOGS_INTERCEPTOR_LOG_LEVEL) {
const levels = env.LOGS_INTERCEPTOR_LOG_LEVEL.split(',')
.map(level => level.trim().toLowerCase())
.filter(level => ['debug', 'info', 'warn', 'error', 'fatal'].includes(level));
if (levels.length > 0) {
config.filter = {
...config.filter,
levels: levels,
};
}
}
if (env.LOGS_INTERCEPTOR_SAMPLING_RATE) {
const samplingRate = parseFloat(env.LOGS_INTERCEPTOR_SAMPLING_RATE);
if (!isNaN(samplingRate) && samplingRate >= 0 && samplingRate <= 1) {
config.filter = {
...config.filter,
samplingRate,
};
}
}
if (env.LOGS_INTERCEPTOR_SANITIZE) {
config.filter = {
...config.filter,
sanitize: env.LOGS_INTERCEPTOR_SANITIZE.toLowerCase() === 'true',
};
}
// Circuit breaker
if (env.LOGS_INTERCEPTOR_CIRCUIT_BREAKER) {
config.circuitBreaker = {
enabled: env.LOGS_INTERCEPTOR_CIRCUIT_BREAKER.toLowerCase() === 'true',
};
}
// Feature flags
if (env.LOGS_INTERCEPTOR_DEBUG) {
config.debug = env.LOGS_INTERCEPTOR_DEBUG.toLowerCase() === 'true';
}
if (env.LOGS_INTERCEPTOR_ENABLED) {
const enabled = env.LOGS_INTERCEPTOR_ENABLED.toLowerCase() === 'true';
if (!enabled) {
// Return a minimal config that effectively disables logging
return {
filter: {
levels: [],
},
};
}
}
return config;
}
exports.loadConfigFromEnv = loadConfigFromEnv;
/**
* Merge configurations with precedence: user config > env config > defaults
*/
function mergeConfigs(userConfig, envConfig) {
const merged = {
...envConfig,
...userConfig,
};
// Handle transport merge carefully
if (envConfig.transport || userConfig.transport) {
merged.transport = {
...envConfig.transport,
...userConfig.transport,
};
}
// Handle other nested objects
if (envConfig.buffer || userConfig.buffer) {
merged.buffer = { ...envConfig.buffer, ...userConfig.buffer };
}
if (envConfig.filter || userConfig.filter) {
merged.filter = { ...envConfig.filter, ...userConfig.filter };
}
if (envConfig.labels || userConfig.labels) {
merged.labels = { ...envConfig.labels, ...userConfig.labels };
}
if (envConfig.dynamicLabels || userConfig.dynamicLabels) {
merged.dynamicLabels = { ...envConfig.dynamicLabels, ...userConfig.dynamicLabels };
}
if (envConfig.circuitBreaker || userConfig.circuitBreaker) {
merged.circuitBreaker = { ...envConfig.circuitBreaker, ...userConfig.circuitBreaker };
}
if (envConfig.integrations || userConfig.integrations) {
merged.integrations = { ...envConfig.integrations, ...userConfig.integrations };
}
if (envConfig.performance || userConfig.performance) {
merged.performance = { ...envConfig.performance, ...userConfig.performance };
}
return merged;
}
exports.mergeConfigs = mergeConfigs;
/**
* Validate that required configuration is present
*/
function validateConfig(config) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
const errors = [];
if (!((_a = config.transport) === null || _a === void 0 ? void 0 : _a.url)) {
errors.push('Transport URL is required (LOGS_INTERCEPTOR_URL)');
}
if (!((_b = config.transport) === null || _b === void 0 ? void 0 : _b.tenantId)) {
errors.push('Tenant ID is required (LOGS_INTERCEPTOR_TENANT_ID)');
}
if (!config.appName) {
errors.push('App name is required (LOGS_INTERCEPTOR_APP_NAME)');
}
// Validate URL format
if ((_c = config.transport) === null || _c === void 0 ? void 0 : _c.url) {
try {
new URL(config.transport.url);
}
catch {
errors.push('Transport URL must be a valid URL');
}
}
// Validate buffer size
if (((_d = config.buffer) === null || _d === void 0 ? void 0 : _d.maxSize) !== undefined && config.buffer.maxSize <= 0) {
errors.push('Buffer max size must be greater than 0');
}
// Validate flush interval
if (((_e = config.buffer) === null || _e === void 0 ? void 0 : _e.flushInterval) !== undefined && config.buffer.flushInterval <= 0) {
errors.push('Flush interval must be greater than 0');
}
// Validate sampling rate
if (((_f = config.filter) === null || _f === void 0 ? void 0 : _f.samplingRate) !== undefined) {
const rate = config.filter.samplingRate;
if (rate < 0 || rate > 1) {
errors.push('Sampling rate must be between 0 and 1');
}
}
// Validate circuit breaker
if (((_g = config.circuitBreaker) === null || _g === void 0 ? void 0 : _g.failureThreshold) !== undefined) {
if (config.circuitBreaker.failureThreshold < 1) {
errors.push('Circuit breaker failure threshold must be at least 1');
}
}
// Validate memory limit
if (((_h = config.buffer) === null || _h === void 0 ? void 0 : _h.maxMemoryMB) !== undefined) {
if (config.buffer.maxMemoryMB < 1 || config.buffer.maxMemoryMB > 1000) {
errors.push('Max memory must be between 1 and 1000 MB');
}
}
// Validate compression level
if (((_j = config.performance) === null || _j === void 0 ? void 0 : _j.compressionLevel) !== undefined) {
const level = config.performance.compressionLevel;
if (level < 0 || level > 9) {
errors.push('Compression level must be between 0 and 9');
}
}
return errors;
}
exports.validateConfig = validateConfig;
/**
* Create a correlation ID for request tracking
*/
function createCorrelationId() {
return crypto.randomBytes(16).toString('hex');
}
exports.createCorrelationId = createCorrelationId;
/**
* Extract metadata from Error objects
*/
function extractErrorMetadata(error) {
const metadata = {
name: error.name,
message: error.message,
stack: error.stack,
};
// Extract additional properties
const errorObj = error;
if (errorObj.code)
metadata.code = errorObj.code;
if (errorObj.statusCode)
metadata.statusCode = errorObj.statusCode;
if (errorObj.syscall)
metadata.syscall = errorObj.syscall;
if (errorObj.errno)
metadata.errno = errorObj.errno;
if (errorObj.path)
metadata.path = errorObj.path;
if (errorObj.address)
metadata.address = errorObj.address;
if (errorObj.port)
metadata.port = errorObj.port;
return metadata;
}
exports.extractErrorMetadata = extractErrorMetadata;
/**
* Parse stack trace to extract useful information
*/
function parseStackTrace(stack) {
const lines = stack.split('\n');
const frames = [];
for (const line of lines) {
const match = line.match(/at\s+(.*?)\s+\((.*?):(\d+):(\d+)\)/);
if (match) {
frames.push({
function: match[1],
file: match[2],
line: parseInt(match[3], 10),
column: parseInt(match[4], 10),
});
}
}
return frames.slice(0, 10); // Limit to 10 frames
}
exports.parseStackTrace = parseStackTrace;
//# sourceMappingURL=utils.js.map