bandeng-logger
Version:
Logger from bandeng developer team.
512 lines (438 loc) • 19.7 kB
JavaScript
/**
* @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
}