logger.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. const fs = require('fs').promises;
  2. const path = require('path');
  3. const util = require('util');
  4. // 日志级别配置
  5. const LOG_LEVELS = {
  6. debug: 0,
  7. info: 1,
  8. success: 2,
  9. warn: 3,
  10. error: 4,
  11. fatal: 5
  12. };
  13. // 默认配置
  14. const config = {
  15. level: process.env.LOG_LEVEL || 'info',
  16. logToFile: process.env.LOG_TO_FILE === 'true',
  17. logDirectory: path.join(__dirname, 'logs'),
  18. logFileName: 'app.log',
  19. maxLogSize: 10 * 1024 * 1024, // 10MB
  20. colorize: process.env.NODE_ENV !== 'production'
  21. };
  22. // 确保日志目录存在
  23. async function ensureLogDirectory() {
  24. if (config.logToFile) {
  25. try {
  26. await fs.access(config.logDirectory);
  27. } catch (error) {
  28. if (error.code === 'ENOENT') {
  29. await fs.mkdir(config.logDirectory, { recursive: true });
  30. console.log(`Created log directory: ${config.logDirectory}`);
  31. } else {
  32. throw error;
  33. }
  34. }
  35. }
  36. }
  37. // 格式化时间 - 改进为更易读的格式
  38. function getTimestamp() {
  39. const now = new Date();
  40. const year = now.getFullYear();
  41. const month = String(now.getMonth() + 1).padStart(2, '0');
  42. const day = String(now.getDate()).padStart(2, '0');
  43. const hours = String(now.getHours()).padStart(2, '0');
  44. const minutes = String(now.getMinutes()).padStart(2, '0');
  45. const seconds = String(now.getSeconds()).padStart(2, '0');
  46. const milliseconds = String(now.getMilliseconds()).padStart(3, '0');
  47. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
  48. }
  49. // 颜色代码
  50. const COLORS = {
  51. reset: '\x1b[0m',
  52. bright: '\x1b[1m',
  53. dim: '\x1b[2m',
  54. underscore: '\x1b[4m',
  55. blink: '\x1b[5m',
  56. reverse: '\x1b[7m',
  57. hidden: '\x1b[8m',
  58. // 前景色
  59. black: '\x1b[30m',
  60. red: '\x1b[31m',
  61. green: '\x1b[32m',
  62. yellow: '\x1b[33m',
  63. blue: '\x1b[34m',
  64. magenta: '\x1b[35m',
  65. cyan: '\x1b[36m',
  66. white: '\x1b[37m',
  67. // 背景色
  68. bgBlack: '\x1b[40m',
  69. bgRed: '\x1b[41m',
  70. bgGreen: '\x1b[42m',
  71. bgYellow: '\x1b[43m',
  72. bgBlue: '\x1b[44m',
  73. bgMagenta: '\x1b[45m',
  74. bgCyan: '\x1b[46m',
  75. bgWhite: '\x1b[47m'
  76. };
  77. // 日志级别对应的颜色和标签
  78. const LEVEL_STYLES = {
  79. debug: { color: COLORS.cyan, label: 'DEBUG' },
  80. info: { color: COLORS.blue, label: 'INFO ' },
  81. success: { color: COLORS.green, label: 'DONE ' },
  82. warn: { color: COLORS.yellow, label: 'WARN ' },
  83. error: { color: COLORS.red, label: 'ERROR' },
  84. fatal: { color: COLORS.bright + COLORS.red, label: 'FATAL' }
  85. };
  86. // 创建日志条目 - 改进格式
  87. function createLogEntry(level, message, meta = {}) {
  88. const timestamp = getTimestamp();
  89. const levelInfo = LEVEL_STYLES[level] || { label: level.toUpperCase() };
  90. // 元数据格式化 - 更简洁的呈现方式
  91. let metaOutput = '';
  92. if (meta instanceof Error) {
  93. metaOutput = `\n ${COLORS.red}Error: ${meta.message}${COLORS.reset}`;
  94. if (meta.stack) {
  95. metaOutput += `\n ${COLORS.dim}Stack: ${meta.stack.split('\n').join('\n ')}${COLORS.reset}`;
  96. }
  97. } else if (Object.keys(meta).length > 0) {
  98. // 检查是否为HTTP请求信息,如果是则使用更简洁的格式
  99. if (meta.ip && meta.userAgent) {
  100. metaOutput = ` ${COLORS.dim}from ${meta.ip}${COLORS.reset}`;
  101. } else {
  102. // 对于其他元数据,仍然使用检查器但格式更友好
  103. metaOutput = `\n ${util.inspect(meta, { colors: true, depth: 3 })}`;
  104. }
  105. }
  106. // 为控制台格式化日志
  107. const consoleOutput = config.colorize ?
  108. `${COLORS.dim}${timestamp}${COLORS.reset} [${levelInfo.color}${levelInfo.label}${COLORS.reset}] ${message}${metaOutput}` :
  109. `${timestamp} [${levelInfo.label}] ${message}${metaOutput ? ' ' + metaOutput.trim() : ''}`;
  110. // 为文件准备JSON格式日志
  111. const logObject = {
  112. timestamp,
  113. level: level,
  114. message
  115. };
  116. if (Object.keys(meta).length > 0) {
  117. logObject.meta = meta instanceof Error ?
  118. { name: meta.name, message: meta.message, stack: meta.stack } :
  119. meta;
  120. }
  121. return {
  122. formatted: consoleOutput,
  123. json: JSON.stringify(logObject)
  124. };
  125. }
  126. // 日志函数
  127. async function log(level, message, meta = {}) {
  128. if (LOG_LEVELS[level] < LOG_LEVELS[config.level]) {
  129. return;
  130. }
  131. const { formatted, json } = createLogEntry(level, message, meta);
  132. // 控制台输出
  133. console.log(formatted);
  134. // 文件日志
  135. if (config.logToFile) {
  136. try {
  137. await ensureLogDirectory();
  138. const logFilePath = path.join(config.logDirectory, config.logFileName);
  139. await fs.appendFile(logFilePath, json + '\n', 'utf8');
  140. } catch (err) {
  141. console.error(`${COLORS.red}Error writing to log file: ${err.message}${COLORS.reset}`);
  142. }
  143. }
  144. }
  145. // 日志API
  146. const logger = {
  147. debug: (message, meta = {}) => log('debug', message, meta),
  148. info: (message, meta = {}) => log('info', message, meta),
  149. success: (message, meta = {}) => log('success', message, meta),
  150. warn: (message, meta = {}) => log('warn', message, meta),
  151. error: (message, meta = {}) => log('error', message, meta),
  152. fatal: (message, meta = {}) => log('fatal', message, meta),
  153. // 配置方法
  154. configure: (options) => {
  155. Object.assign(config, options);
  156. },
  157. // HTTP请求日志方法 - 简化输出格式
  158. request: (req, res, duration) => {
  159. const status = res.statusCode;
  160. const method = req.method;
  161. const url = req.originalUrl || req.url;
  162. const userAgent = req.headers['user-agent'] || '-';
  163. const ip = req.ip || req.connection.remoteAddress || '-';
  164. let level = 'info';
  165. if (status >= 500) level = 'error';
  166. else if (status >= 400) level = 'warn';
  167. // 为HTTP请求创建更简洁的日志消息
  168. let statusIndicator = '';
  169. if (config.colorize) {
  170. if (status >= 500) statusIndicator = COLORS.red;
  171. else if (status >= 400) statusIndicator = COLORS.yellow;
  172. else if (status >= 300) statusIndicator = COLORS.cyan;
  173. else if (status >= 200) statusIndicator = COLORS.green;
  174. statusIndicator += status + COLORS.reset;
  175. } else {
  176. statusIndicator = status;
  177. }
  178. // 简化的请求日志格式
  179. const message = `${method} ${url} ${statusIndicator} ${duration}ms`;
  180. // 传递ip和userAgent作为元数据,但以简洁方式显示
  181. log(level, message, { ip, userAgent });
  182. }
  183. };
  184. // 初始化
  185. ensureLogDirectory().catch(err => {
  186. console.error(`${COLORS.red}Failed to initialize logger: ${err.message}${COLORS.reset}`);
  187. });
  188. module.exports = logger;