UNPKG

bandeng-logger

Version:

Logger from bandeng developer team.

512 lines (438 loc) 19.7 kB
'use strict' /** * @description 日志写入性能监控函数(优化版,避免内存泄漏) * @param {string} level - 日志级别 * @param {string} message - 日志消息 * @param {Object} stats - 统计信息 * @returns {Function} 性能监控函数 */ function measureLogPerformance(level, message, stats) { // Node.js 12 兼容性:使用普通 hrtime 而不是 bigint const startTime = process.hrtime() // 返回 [秒, 纳秒] const maxLatency = 100 // 使用默认的时间阈值 const shortMessage = message.length > 100 ? message.substring(0, 100) + '...' : message // 创建带超时的性能监控函数 const measureFunction = () => { try { const endTime = process.hrtime() // 计算时间差:(结束秒 - 开始秒) * 1000 + (结束纳秒 - 开始纳秒) / 1000000 const latencyMs = (endTime[0] - startTime[0]) * 1000 + (endTime[1] - startTime[1]) / 1000000 if (latencyMs > maxLatency) { // 预构建性能警告消息,避免频繁的字符串拼接 const performanceMsg = `[LOGGER_PERFORMANCE] Log writing took ${latencyMs.toFixed( 2 )}ms (threshold: ${maxLatency}ms), Level: ${level}, Message: ${shortMessage}` console.warn(performanceMsg) // 更新统计信息 if (stats) { stats.slowWrites = (stats.slowWrites || 0) + 1 } } } catch (error) { // 静默处理性能监控错误,避免影响主流程 } } // 设置超时自动清理(防止measure()没有被调用) const timeoutId = setTimeout(() => { console.warn(`[LOGGER_WARNING] Performance measure timeout for level: ${level}`) }, 30000) // 30秒超时 // 在measureFunction中清除超时 const originalMeasure = measureFunction measureFunction._timeoutId = timeoutId return () => { if (measureFunction._timeoutId) { clearTimeout(measureFunction._timeoutId) measureFunction._timeoutId = undefined } originalMeasure() } } /** * @description 性能日志记录 * @param {string} source - 来源 * @param {string} path - 路由 * @param {number} latency - 耗时 * @param {number} timeThreshold - 阈值 * @param {Object} reqObj - 请求参数对象 * @param {Function} warn - 警告日志函数 * @param {Function} trace - 跟踪日志函数 * @param {boolean} tracePerformanceDeprecationWarned - 废弃警告标志 * @returns {boolean} 更新后的废弃警告标志 */ function tracePerformance(source, path, latency, timeThreshold, reqObj, warn, trace, tracePerformanceDeprecationWarned) { // 弃用警告:只打印一次到控制台,不写入日志文件 if (!tracePerformanceDeprecationWarned) { console.warn( '[DEPRECATED] Logger.tracePerformance() is deprecated and will be removed in a future version.\nPlease use Logger.traceRequestPerformance() instead.\nExample: logger.traceRequestPerformance({source, path, reqObj}, latency, timeThreshold)' ) tracePerformanceDeprecationWarned = true } if (!source) { source = 'anonymous client' } if (latency >= timeThreshold) { let strReqObj = JSON.stringify(reqObj) const maxLength = 1000 if (strReqObj.length > maxLength) { strReqObj = strReqObj.substring(0, maxLength) + '...' } warn({ source, path, latency, queryObj: strReqObj, message: `Poor performance request, ${latency} ms.` }) return tracePerformanceDeprecationWarned } trace({ source, path, latency, message: `latency ${latency} ms` }) return tracePerformanceDeprecationWarned } /** * @description 请求性能追踪日志 * @param {Object} ctx - 请求上下文(由外部调用者决定内容格式) * @param {number} latency - 请求耗时(毫秒) * @param {number} timeThreshold - 生产环境打印请求耗时的阈值(毫秒) * @param {Function} ctxProcessor - 上下文处理函数(用户配置的处理器或默认的sanitizeContextForLogging) * @param {boolean} [forcePrint=false] - 是否强制打印,true时忽略日志级别配置直接打印所有请求 * @param {string} [tag='traceRequestPerformance'] - 自定义标签,用于筛选日志,默认为 'traceRequestPerformance' * @returns {Object} 处理后的性能日志数据,包含日志级别和消息 */ function traceRequestPerformance(ctx, latency, timeThreshold, ctxProcessor, forcePrint = false, tag = 'traceRequestPerformance') { if (!ctx || typeof ctx !== 'object') { console.debug('[logger] Invalid context provided to traceRequestPerformance') return null } // 对ctx内容进行处理:使用注入的ctxProcessor或默认的sanitizeContextForLogging let sanitizedCtx if (typeof ctxProcessor === 'function') { // 如果传入的是函数(可能是用户自定义的ctxProcessor) try { sanitizedCtx = ctxProcessor(ctx) } catch (error) { // 使用 console.error 而不是 console.warn,确保错误信息能被看到 console.error('[logger] Error applying ctxProcessor:', error.message, '\nStack trace:\n' + error.stack) sanitizedCtx = sanitizeContextForLogging(ctx) } } else { // 使用默认的sanitizeContextForLogging sanitizedCtx = sanitizeContextForLogging(ctx) } // 如果tag不是字符串或者是空字符串或者是空格 if (typeof tag !== 'string' || tag === '' || tag.trim() === '') { tag = 'traceRequestPerformance' } // 构建性能日志对象 const performanceLog = { latency: latency, ...sanitizedCtx // 使用处理后的ctx内容 } // 如果启用了强制打印,忽略阈值判断直接使用info级别确保能被打印 if (forcePrint === true) { return { level: 'info', // 使用info级别确保在大多数配置下都能被打印 data: { ...performanceLog, tag: tag, message: `Request completed in ${latency} ms`, threshold: timeThreshold, forced: true // 标记为强制打印 } } } // 根据性能阈值决定日志级别和消息 if (latency >= timeThreshold) { // 高延迟请求,使用warn级别 return { level: 'warn', data: { ...performanceLog, tag: tag, message: `Slow request detected, completed in ${latency} ms`, threshold: timeThreshold } } } else { // 正常请求,使用trace级别 return { level: 'trace', data: { ...performanceLog, tag: tag, message: `Request completed in ${latency} ms` } } } } /** * @description 对日志上下文内容进行安全处理和长度限制 * @param {Object} ctx - 原始上下文对象 * @param {Object} options - 配置选项 * @returns {Object} 处理后的安全上下文对象 */ function sanitizeContextForLogging(ctx, options = {}) { if (!ctx || typeof ctx !== 'object') { return {} } const sanitized = {} const MAX_STRING_LENGTH = options.maxStringLength || 1000 const MAX_ARRAY_LENGTH = options.maxArrayLength || 50 for (const [key, value] of Object.entries(ctx)) { try { if (typeof value === 'string') { // 字符串长度限制 sanitized[key] = value.length > MAX_STRING_LENGTH ? value.substring(0, MAX_STRING_LENGTH) + `...[truncated ${value.length - MAX_STRING_LENGTH} chars]` : value } else if (Array.isArray(value)) { // 数组长度限制 if (value.length > MAX_ARRAY_LENGTH) { sanitized[key] = value.slice(0, MAX_ARRAY_LENGTH) sanitized[key].push(`...[truncated ${value.length - MAX_ARRAY_LENGTH} items]`) } else { // 对数组中的每个元素进行递归处理 sanitized[key] = value.map(item => { if (typeof item === 'string' && item.length > MAX_STRING_LENGTH) { return item.substring(0, MAX_STRING_LENGTH) + `...[truncated ${item.length - MAX_STRING_LENGTH} chars]` } else if (typeof item === 'object' && item !== null) { return sanitizeContextForLogging(item, options) } return item }) } } else if (typeof value === 'object' && value !== null) { // 递归处理嵌套对象,但避免循环引用 if (value.constructor === Object) { sanitized[key] = sanitizeContextForLogging(value, options) } else { // 对于其他对象类型(如Date, Error等),转换为字符串 sanitized[key] = String(value) } } else if (typeof value === 'function' || value instanceof Buffer) { // 过滤掉函数和Buffer对象 sanitized[key] = `[${typeof value}]` } else { // 其他基本类型直接保留 sanitized[key] = value } } catch (error) { // 如果处理某个字段出错,使用占位符 sanitized[key] = '[processing error]' } } return sanitized } /** * @description 获取预定义的 ctx 处理函数 * @param {string} type - 处理函数类型 * @param {Object} options - 处理选项 * @returns {Function} 处理函数 */ function getCtxProcessor(type, options = {}) { const processors = { // 只保留指定的字段(支持嵌套路径,如 'user.name') whitelist: fields => { const fieldPaths = (Array.isArray(fields) ? fields : [fields]).map(field => { return typeof field === 'string' ? field.split('.') : [field] }) const processed = new WeakSet() // 检查路径是否匹配白名单 const isPathAllowed = path => { // 精确匹配:如果当前路径完全匹配任何允许路径 const exactMatch = fieldPaths.some(allowedPath => { if (allowedPath.length !== path.length) return false for (let i = 0; i < allowedPath.length; i++) { if (allowedPath[i] !== path[i]) return false } return true }) if (exactMatch) return true // 前缀匹配:如果当前路径是任何允许路径的前缀 // 例如:如果允许 'user.name',那么路径 'user' 是它的前缀,应该被允许 return fieldPaths.some(allowedPath => { if (allowedPath.length <= path.length) return false for (let i = 0; i < path.length; i++) { if (allowedPath[i] !== path[i]) return false } return true }) } const processObject = (obj, currentPath = []) => { if (!obj || typeof obj !== 'object') return obj if (processed.has(obj)) { return '[Circular Reference]' } processed.add(obj) if (Array.isArray(obj)) { const result = obj.map((item, index) => processObject(item, [...currentPath, index.toString()])) processed.delete(obj) return result } const result = {} for (const [key, value] of Object.entries(obj)) { const fieldPath = [...currentPath, key] // 检查这个字段路径是否被允许 if (isPathAllowed(fieldPath)) { result[key] = processObject(value, fieldPath) } } processed.delete(obj) return result } return ctx => processObject(ctx, []) }, // 排除指定的字段(支持嵌套路径,如 'user.password') blacklist: fields => { const fieldPaths = (Array.isArray(fields) ? fields : [fields]).map(field => { return typeof field === 'string' ? field.split('.') : [field] }) const processed = new WeakSet() // 检查路径是否在黑名单中 const isPathBlocked = path => { return fieldPaths.some(blockedPath => { // 检查阻止路径是否是当前路径的前缀 // 例如:如果阻止 'user.password',那么 'user.password' 应该被阻止 // 如果阻止 'config',那么 'config', 'config.apiKey', 'config.database' 都应该被阻止 if (blockedPath.length > path.length) return false for (let i = 0; i < blockedPath.length; i++) { if (blockedPath[i] !== path[i]) return false } return true }) } const processObject = (obj, currentPath = []) => { if (!obj || typeof obj !== 'object') return obj if (processed.has(obj)) { return '[Circular Reference]' } processed.add(obj) if (Array.isArray(obj)) { const result = obj.map((item, index) => processObject(item, [...currentPath, index.toString()])) processed.delete(obj) return result } const result = {} for (const [key, value] of Object.entries(obj)) { const fieldPath = [...currentPath, key] // 检查这个字段路径是否被阻止 if (!isPathBlocked(fieldPath)) { result[key] = processObject(value, fieldPath) } // 如果字段在黑名单中,则跳过(不添加到结果中) } processed.delete(obj) return result } return ctx => processObject(ctx, []) }, // 字段重命名(递归处理嵌套对象) rename: mapping => { const processed = new WeakSet() const processObject = obj => { if (!obj || typeof obj !== 'object') return obj if (processed.has(obj)) { return '[Circular Reference]' } processed.add(obj) if (Array.isArray(obj)) { const result = obj.map(item => processObject(item)) processed.delete(obj) return result } const result = {} for (const [key, value] of Object.entries(obj)) { const newKey = mapping[key] || key result[newKey] = processObject(value) } processed.delete(obj) return result } return ctx => processObject(ctx) }, // 字段映射和过滤(递归处理嵌套对象) transform: config => { const {include = [], exclude = [], rename = {}} = config const includeSet = include.length > 0 ? new Set(include) : null const excludeSet = exclude.length > 0 ? new Set(exclude) : null const processed = new WeakSet() const processObject = obj => { if (!obj || typeof obj !== 'object') return obj if (processed.has(obj)) { return '[Circular Reference]' } processed.add(obj) if (Array.isArray(obj)) { const result = obj.map(item => processObject(item)) processed.delete(obj) return result } const result = {} for (const [key, value] of Object.entries(obj)) { // 检查排除列表 if (excludeSet && excludeSet.has(key)) { continue } // 检查包含列表(如果指定了包含列表) if (includeSet && !includeSet.has(key)) { continue } // 重命名字段 const newKey = rename[key] || key result[newKey] = processObject(value) } processed.delete(obj) return result } return ctx => processObject(ctx) }, // 深度限制(避免循环引用) depthLimit: (maxDepth = 3) => { const processed = new WeakSet() const processValue = (value, currentDepth = 0) => { if (currentDepth >= maxDepth) { return typeof value === 'object' && value !== null ? '[Depth Limit]' : value } if (value === null || typeof value !== 'object') { return value } if (processed.has(value)) { return '[Circular Reference]' } processed.add(value) if (Array.isArray(value)) { return value.map(item => processValue(item, currentDepth + 1)) } const result = {} for (const [key, val] of Object.entries(value)) { result[key] = processValue(val, currentDepth + 1) } processed.delete(value) return result } return ctx => processValue(ctx, 0) }, // 组合多个处理器 combine: (...processors) => { return ctx => { return processors.reduce((result, processor) => processor(result), ctx) } } } if (!processors[type]) { throw new Error(`Unknown ctx processor type: ${type}. Available types: ${Object.keys(processors).join(', ')}`) } return processors[type](options) } module.exports = { measureLogPerformance, tracePerformance, traceRequestPerformance, sanitizeContextForLogging, getCtxProcessor }