bandeng-logger
Version:
Logger from bandeng developer team.
380 lines (345 loc) • 14.3 kB
JavaScript
const path = require('path')
// 日志级别常量
const LOG_LEVELS = {
fatal: 0,
error: 1,
warn: 2,
info: 3,
debug: 4,
trace: 5
}
// 警告函数
function configWarn(message) {
console.warn(`[logger] ${message}`)
}
/**
* @description 合并配置选项
* @param {Object} userOptions - 用户提供的配置
* @returns {Object} 合并后的配置
*/
function mergeOptions(userOptions) {
try {
// 参数验证
if (userOptions !== undefined && typeof userOptions !== 'object') {
throw new Error('userOptions must be an object or undefined')
}
const defaultOptions = {
logLevel: 'warn',
logFormat: 'json',
utcFormat: -Number((new Date().getTimezoneOffset() / 60).toFixed(1)),
maxLength: 1000,
logDir: path.resolve(path.join(process.cwd(), 'logs')), // 确保是绝对路径
logFileName: 'server.log',
errorLogFileName: 'error.log',
separateErrorLog: true,
separateErrorLevels: ['error', 'fatal'], // 添加默认值
traceLogWrite: false,
rotate: 100,
interval: 3600,
compress: 'none',
maxFiles: 15,
bufferSize: 64 * 1024,
flushInterval: 1000,
forceSyncLevels: ['fatal'],
ctxProcessor: null, // ctx 字段处理函数,默认使用内置的 sanitizeContextForLogging
forcePrintConditions: [], // 强制打印条件,无论日志级别如何都打印匹配的日志
colors: {
fatal: 'brightRed',
error: 'red',
warn: 'brightYellow',
info: 'gray',
debug: 'blue',
trace: 'white'
}
}
// 合并选项,使用展开运算符
const mergedOptions = {...defaultOptions, ...(userOptions || {})}
// 确保logDir是绝对路径
if (mergedOptions.logDir && typeof mergedOptions.logDir === 'string') {
try {
mergedOptions.logDir = path.resolve(mergedOptions.logDir)
} catch (pathError) {
throw new Error(`Invalid logDir path: ${mergedOptions.logDir}`)
}
}
return mergedOptions
} catch (error) {
throw new Error(`Configuration merge failed: ${error.message}`)
}
}
/**
* @description 验证配置参数
* @param {Object} options - 配置参数
* @throws {Error} 当配置参数无效时抛出错误
*/
function validateOptions(options) {
try {
// 参数验证
if (!options || typeof options !== 'object') {
throw new Error('options must be a valid object')
}
const validations = {
logLevel: {
type: 'string',
validValues: ['fatal', 'error', 'warn', 'info', 'debug', 'trace'],
message: 'logLevel must be one of: fatal, error, warn, info, debug, trace'
},
logFormat: {
type: 'string',
validValues: ['json', 'text'],
message: 'logFormat must be either "json" or "text"'
},
utcFormat: {
type: 'number',
range: [-12, 14],
message: 'utcFormat must be a number between -12 and 14'
},
maxLength: {
type: 'number',
min: 1,
message: 'maxLength must be a positive number'
},
logDir: {
type: 'string',
message: 'logDir must be a string'
},
logFileName: {
type: 'string',
min: 1,
max: 255,
pattern: /^[a-zA-Z0-9_.-]+\.log$/,
message: 'logFileName must be a valid filename (1-255 chars) ending with .log'
},
errorLogFileName: {
type: 'string',
min: 1,
max: 255,
pattern: /^[a-zA-Z0-9_.-]+\.log$/,
message: 'errorLogFileName must be a valid filename (1-255 chars) ending with .log'
},
separateErrorLog: {
type: 'boolean',
message: 'separateErrorLog must be a boolean'
},
traceLogWrite: {
type: 'boolean',
message: 'traceLogWrite must be a boolean'
},
rotate: {
type: 'number',
min: 1,
message: 'rotate must be a positive number'
},
interval: {
type: 'number',
min: 60, // 最小间隔1分钟
max: 604800, // 最大间隔7天
message: 'interval must be a number between 60 and 604800 seconds (1 minute to 7 days)'
},
compress: {
type: 'string',
validValues: ['gzip', 'none'],
message: 'compress must be either "gzip" or "none"'
},
maxFiles: {
type: 'number',
min: 1,
message: 'maxFiles must be a positive number'
},
colors: {
type: 'object',
message: 'colors must be an object mapping log levels to color names'
},
bufferSize: {
type: 'number',
min: 1024, // 最小1KB
max: 1024 * 1024 * 10, // 最大10MB
message: 'bufferSize must be a number between 1024 and 10485760 bytes (1KB to 10MB)'
},
flushInterval: {
type: 'number',
min: 100, // 最小100ms
max: 60000, // 最大1分钟
message: 'flushInterval must be a number between 100 and 60000 milliseconds (100ms to 1 minute)'
},
forceSyncLevels: {
type: 'array',
message: 'forceSyncLevels must be an array of log levels',
validator: value => value.every(level => ['fatal', 'error', 'warn', 'info', 'debug', 'trace'].includes(level))
},
separateErrorLevels: {
type: 'array',
message: 'separateErrorLevels must be an array of log levels',
validator: value => value.every(level => ['fatal', 'error', 'warn', 'info', 'debug', 'trace'].includes(level))
},
ctxProcessor: {
type: 'function',
message: 'ctxProcessor must be a function or null',
validator: value => value === null || typeof value === 'function'
},
forcePrintConditions: {
type: 'array',
message: 'forcePrintConditions must be an array of functions',
validator: value => Array.isArray(value) && value.every(item => typeof item === 'function')
}
}
for (const [key, validation] of Object.entries(validations)) {
const value = options[key]
// 只有在值不为undefined且不为false时才验证(允许使用默认值)
if (value !== undefined && value !== false) {
// 检查null值是否被允许
if (value === null) {
if (!validation.validator || !validation.validator(null)) {
throw new Error(`${key} cannot be null`)
}
// null值被验证器允许,跳过进一步验证
continue
}
// 检查类型(精确判断)
let actualType = typeof value
if (Array.isArray(value)) {
actualType = 'array' // 数组有自己的类型
} else if (actualType === 'object' && value !== null) {
actualType = 'object' // null已经被上面处理了
}
if (actualType !== validation.type) {
throw new Error(`${key} must be of type ${validation.type}, got ${actualType}`)
}
// 检查数值范围(只对number类型)
if (validation.type === 'number') {
if (validation.min !== undefined && value < validation.min) {
throw new Error(validation.message)
}
if (validation.max !== undefined && value > validation.max) {
throw new Error(validation.message)
}
if (validation.range !== undefined && (value < validation.range[0] || value > validation.range[1])) {
throw new Error(validation.message)
}
// 检查是否为有限数字
if (!Number.isFinite(value)) {
throw new Error(`${key} must be a finite number`)
}
}
// 检查字符串长度(只对string类型)
if (validation.type === 'string') {
if (validation.min !== undefined && value.length < validation.min) {
throw new Error(`${key} must be at least ${validation.min} characters long`)
}
if (validation.max !== undefined && value.length > validation.max) {
throw new Error(`${key} must be at most ${validation.max} characters long`)
}
}
// 检查有效值列表
if (validation.validValues && !validation.validValues.includes(value)) {
throw new Error(validation.message)
}
// 检查正则表达式模式
if (validation.pattern && !validation.pattern.test(value)) {
throw new Error(validation.message)
}
// 检查自定义验证器
if (validation.validator && !validation.validator(value)) {
throw new Error(validation.message)
}
}
}
} catch (error) {
throw new Error(`Configuration validation failed: ${error.message}`)
}
}
/**
* @description 验证和清理配置选项
* @param {Object} options - 用户提供的配置
* @returns {Object} 验证和清理后的配置
*/
function validateAndCleanOptions(options) {
const cleanedOptions = {...options}
// 验证并清理日志级别
if (cleanedOptions.logLevel) {
cleanedOptions.logLevel = cleanedOptions.logLevel.toLowerCase()
if (!LOG_LEVELS[cleanedOptions.logLevel]) {
configWarn(`Invalid log level "${options.logLevel}", defaulting to "warn"`)
cleanedOptions.logLevel = 'warn'
}
}
// 验证并清理UTC偏移
if (typeof cleanedOptions.utcFormat === 'number') {
if (cleanedOptions.utcFormat < -12 || cleanedOptions.utcFormat > 14) {
configWarn(`Invalid UTC offset "${options.utcFormat}", defaulting to 8`)
cleanedOptions.utcFormat = 8
}
}
// 验证并清理最大长度
if (typeof cleanedOptions.maxLength === 'number') {
if (cleanedOptions.maxLength < 1) {
configWarn(`Invalid max length "${options.maxLength}", defaulting to 1000`)
cleanedOptions.maxLength = 1000
}
}
// 验证并清理轮转大小
if (typeof cleanedOptions.rotate === 'number') {
if (cleanedOptions.rotate < 1) {
configWarn(`Invalid rotate size "${options.rotate}", defaulting to 100`)
cleanedOptions.rotate = 100
}
}
// 验证并清理检查间隔
if (typeof cleanedOptions.interval === 'number') {
if (cleanedOptions.interval < 60 || cleanedOptions.interval > 604800) {
configWarn(`Invalid interval "${options.interval}", defaulting to 3600`)
cleanedOptions.interval = 3600
}
}
// 验证并清理缓冲区大小
if (typeof cleanedOptions.bufferSize === 'number') {
const minBufferSize = 1024
const maxBufferSize = 100 * 1024 * 1024 // 100MB
if (cleanedOptions.bufferSize < minBufferSize || cleanedOptions.bufferSize > maxBufferSize) {
configWarn(`Invalid buffer size "${options.bufferSize}", defaulting to 65536`)
cleanedOptions.bufferSize = 65536
}
}
// 验证并清理刷新间隔
if (typeof cleanedOptions.flushInterval === 'number') {
if (cleanedOptions.flushInterval < 100 || cleanedOptions.flushInterval > 60000) {
configWarn(`Invalid flush interval "${options.flushInterval}", defaulting to 1000`)
cleanedOptions.flushInterval = 1000
}
}
// 验证颜色配置
if (cleanedOptions.colors && typeof cleanedOptions.colors === 'object') {
const validColors = [
'black',
'red',
'green',
'yellow',
'blue',
'magenta',
'cyan',
'white',
'gray',
'brightBlack',
'brightRed',
'brightGreen',
'brightYellow',
'brightBlue',
'brightMagenta',
'brightCyan',
'brightWhite'
]
for (const [level, color] of Object.entries(cleanedOptions.colors)) {
if (!validColors.includes(color)) {
configWarn(`Invalid color "${color}" for level "${level}", using default`)
delete cleanedOptions.colors[level]
}
}
}
return cleanedOptions
}
module.exports = {
mergeOptions,
validateOptions,
validateAndCleanOptions
}