ProductionLogger.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import { createWriteStream, existsSync, mkdirSync } from 'fs'
  2. import { join } from 'path'
  3. export class ProductionLogger {
  4. constructor(config = {}) {
  5. this.currentFileSize = 0
  6. this.config = {
  7. level: 'info',
  8. enableConsole: true,
  9. enableFile: true,
  10. logDir: './logs',
  11. maxFileSize: 100,
  12. maxFiles: 10,
  13. enableRotation: true,
  14. enableAudit: true,
  15. ...config,
  16. }
  17. this.initializeLogDirectory()
  18. this.initializeLogStreams()
  19. }
  20. /**
  21. * 获取单例实例
  22. */
  23. static getInstance(config) {
  24. if (!ProductionLogger.instance) {
  25. ProductionLogger.instance = new ProductionLogger(config)
  26. }
  27. return ProductionLogger.instance
  28. }
  29. /**
  30. * 初始化日志目录
  31. */
  32. initializeLogDirectory() {
  33. if (this.config.enableFile && this.config.logDir) {
  34. if (!existsSync(this.config.logDir)) {
  35. mkdirSync(this.config.logDir, { recursive: true })
  36. }
  37. }
  38. }
  39. /**
  40. * 初始化日志流
  41. */
  42. initializeLogStreams() {
  43. if (this.config.enableFile && this.config.logDir) {
  44. const timestamp = new Date().toISOString().split('T')[0]
  45. // 主日志文件
  46. this.currentLogFile = join(this.config.logDir, `trading-system-${timestamp}.log`)
  47. this.logStream = createWriteStream(this.currentLogFile, { flags: 'a' })
  48. // 审计日志文件
  49. if (this.config.enableAudit) {
  50. const auditFile = join(this.config.logDir, `audit-${timestamp}.log`)
  51. this.auditStream = createWriteStream(auditFile, { flags: 'a' })
  52. }
  53. }
  54. }
  55. /**
  56. * 记录 Debug 级别日志
  57. */
  58. debug(message, metadata, module = 'SYSTEM') {
  59. this.log('debug', module, message, metadata)
  60. }
  61. /**
  62. * 记录 Info 级别日志
  63. */
  64. info(message, metadata, module = 'SYSTEM') {
  65. this.log('info', module, message, metadata)
  66. }
  67. /**
  68. * 记录 Warning 级别日志
  69. */
  70. warn(message, metadata, module = 'SYSTEM') {
  71. this.log('warn', module, message, metadata)
  72. }
  73. /**
  74. * 记录 Error 级别日志
  75. */
  76. error(message, error, metadata, module = 'SYSTEM') {
  77. const errorMetadata = {
  78. ...metadata,
  79. ...(error && {
  80. error: {
  81. name: error.name,
  82. message: error.message,
  83. stack: error.stack,
  84. },
  85. }),
  86. }
  87. this.log('error', module, message, errorMetadata)
  88. }
  89. /**
  90. * 记录 Critical 级别日志
  91. */
  92. critical(message, error, metadata, module = 'SYSTEM') {
  93. const errorMetadata = {
  94. ...metadata,
  95. ...(error && {
  96. error: {
  97. name: error.name,
  98. message: error.message,
  99. stack: error.stack,
  100. },
  101. }),
  102. }
  103. this.log('critical', module, message, errorMetadata)
  104. }
  105. /**
  106. * 记录交易相关日志
  107. */
  108. trade(action, details) {
  109. this.log('info', 'TRADING', `交易操作: ${action}`, {
  110. ...details,
  111. category: 'trading',
  112. })
  113. }
  114. /**
  115. * 记录对冲相关日志
  116. */
  117. hedge(action, details) {
  118. this.log('info', 'HEDGING', `对冲操作: ${action}`, {
  119. ...details,
  120. category: 'hedging',
  121. })
  122. }
  123. /**
  124. * 记录账户相关日志
  125. */
  126. account(action, details) {
  127. this.log('info', 'ACCOUNT', `账户操作: ${action}`, {
  128. ...details,
  129. category: 'account',
  130. })
  131. }
  132. /**
  133. * 记录 WebSocket 相关日志
  134. */
  135. websocket(action, details) {
  136. this.log('info', 'WEBSOCKET', `WebSocket事件: ${action}`, {
  137. ...details,
  138. category: 'websocket',
  139. })
  140. }
  141. /**
  142. * 记录性能相关日志
  143. */
  144. performance(metric, value, details) {
  145. this.log('info', 'PERFORMANCE', `性能指标: ${metric}`, {
  146. metric,
  147. value,
  148. unit: 'ms',
  149. ...details,
  150. category: 'performance',
  151. })
  152. }
  153. /**
  154. * 记录审计日志
  155. */
  156. audit(action, details) {
  157. const auditEntry = {
  158. timestamp: new Date().toISOString(),
  159. action,
  160. ...details,
  161. category: 'audit',
  162. }
  163. // 同时写入主日志和审计日志
  164. this.log('info', 'AUDIT', `审计操作: ${action}`, auditEntry)
  165. if (this.auditStream) {
  166. this.auditStream.write(JSON.stringify(auditEntry) + '\n')
  167. }
  168. }
  169. /**
  170. * 核心日志记录方法
  171. */
  172. log(level, module, message, metadata) {
  173. // 检查日志级别
  174. if (!this.shouldLog(level)) {
  175. return
  176. }
  177. const logEntry = {
  178. timestamp: new Date().toISOString(),
  179. level,
  180. module,
  181. message,
  182. metadata,
  183. }
  184. // 控制台输出
  185. if (this.config.enableConsole) {
  186. this.outputToConsole(logEntry)
  187. }
  188. // 文件输出
  189. if (this.config.enableFile && this.logStream) {
  190. this.outputToFile(logEntry)
  191. }
  192. // 检查文件轮转
  193. if (this.config.enableRotation && this.shouldRotate()) {
  194. this.rotateLogFile()
  195. }
  196. }
  197. /**
  198. * 检查是否应该记录该级别的日志
  199. */
  200. shouldLog(level) {
  201. const levels = ['debug', 'info', 'warn', 'error', 'critical']
  202. const currentLevelIndex = levels.indexOf(this.config.level)
  203. const messageLevelIndex = levels.indexOf(level)
  204. return messageLevelIndex >= currentLevelIndex
  205. }
  206. /**
  207. * 输出到控制台
  208. */
  209. outputToConsole(entry) {
  210. const coloredLevel = this.getColoredLevel(entry.level)
  211. const timestamp = entry.timestamp.split('T')[1].split('.')[0] // 只显示时间部分
  212. let output = `${timestamp} ${coloredLevel} [${entry.module}] ${entry.message}`
  213. if (entry.metadata) {
  214. const metadataStr = JSON.stringify(entry.metadata, null, 2)
  215. .split('\n')
  216. .map(line => ` ${line}`)
  217. .join('\n')
  218. output += `\n${metadataStr}`
  219. }
  220. // 根据级别选择输出方式
  221. switch (entry.level) {
  222. case 'error':
  223. case 'critical':
  224. console.error(output)
  225. break
  226. case 'warn':
  227. console.warn(output)
  228. break
  229. default:
  230. console.log(output)
  231. }
  232. }
  233. /**
  234. * 输出到文件
  235. */
  236. outputToFile(entry) {
  237. if (!this.logStream) return
  238. const logLine = JSON.stringify(entry) + '\n'
  239. this.logStream.write(logLine)
  240. this.currentFileSize += Buffer.byteLength(logLine)
  241. }
  242. /**
  243. * 获取带颜色的日志级别
  244. */
  245. getColoredLevel(level) {
  246. const colors = {
  247. debug: '\x1b[90m',
  248. info: '\x1b[32m',
  249. warn: '\x1b[33m',
  250. error: '\x1b[31m',
  251. critical: '\x1b[41m', // 红色背景
  252. }
  253. const reset = '\x1b[0m'
  254. return `${colors[level]}${level.toUpperCase().padEnd(8)}${reset}`
  255. }
  256. /**
  257. * 检查是否需要轮转日志文件
  258. */
  259. shouldRotate() {
  260. const maxSizeBytes = (this.config.maxFileSize || 100) * 1024 * 1024
  261. return this.currentFileSize > maxSizeBytes
  262. }
  263. /**
  264. * 轮转日志文件
  265. */
  266. rotateLogFile() {
  267. if (this.logStream) {
  268. this.logStream.end()
  269. }
  270. if (this.auditStream) {
  271. this.auditStream.end()
  272. }
  273. this.currentFileSize = 0
  274. this.initializeLogStreams()
  275. this.info(
  276. '日志文件已轮转',
  277. {
  278. newLogFile: this.currentLogFile,
  279. },
  280. 'LOGGER',
  281. )
  282. }
  283. /**
  284. * 创建子日志器
  285. */
  286. child(module, baseMetadata) {
  287. return {
  288. debug: (message, metadata) => this.debug(message, { ...baseMetadata, ...metadata }, module),
  289. info: (message, metadata) => this.info(message, { ...baseMetadata, ...metadata }, module),
  290. warn: (message, metadata) => this.warn(message, { ...baseMetadata, ...metadata }, module),
  291. error: (message, error, metadata) => this.error(message, error, { ...baseMetadata, ...metadata }, module),
  292. critical: (message, error, metadata) => this.critical(message, error, { ...baseMetadata, ...metadata }, module),
  293. }
  294. }
  295. /**
  296. * 关闭日志器
  297. */
  298. close() {
  299. if (this.logStream) {
  300. this.logStream.end()
  301. }
  302. if (this.auditStream) {
  303. this.auditStream.end()
  304. }
  305. }
  306. /**
  307. * 获取统计信息
  308. */
  309. getStats() {
  310. return {
  311. config: this.config,
  312. currentLogFile: this.currentLogFile,
  313. currentFileSize: this.currentFileSize,
  314. streamActive: !!this.logStream && !this.logStream.destroyed,
  315. }
  316. }
  317. }
  318. // 创建默认的全局日志器实例
  319. export const logger = ProductionLogger.getInstance({
  320. level: process.env.LOG_LEVEL || 'info',
  321. enableConsole: process.env.NODE_ENV !== 'test',
  322. enableFile: process.env.NODE_ENV === 'production',
  323. logDir: process.env.LOG_DIR || './logs',
  324. })