|
| 1 | +# 日志系统实现指南 |
| 2 | + |
| 3 | +自定义日志系统的完整实现。 |
| 4 | + |
| 5 | +## 核心实现 |
| 6 | + |
| 7 | +### 1. 类型定义 |
| 8 | + |
| 9 | +```typescript |
| 10 | +// src/logging/types.ts |
| 11 | + |
| 12 | +export type LogLevel = 'debug' | 'info' | 'success' | 'warn' | 'error' |
| 13 | + |
| 14 | +export interface LoggerOptions { |
| 15 | + level?: LogLevel |
| 16 | + file?: string |
| 17 | + timestamp?: boolean |
| 18 | +} |
| 19 | + |
| 20 | +export interface LogData { |
| 21 | + [key: string]: unknown |
| 22 | +} |
| 23 | + |
| 24 | +export interface Logger { |
| 25 | + debug: (message: string, data?: LogData) => void |
| 26 | + info: (message: string, data?: LogData) => void |
| 27 | + success: (message: string, data?: LogData) => void |
| 28 | + warn: (message: string, data?: LogData) => void |
| 29 | + error: (message: string, error?: Error | LogData) => void |
| 30 | + setLevel: (level: LogLevel) => void |
| 31 | +} |
| 32 | +``` |
| 33 | + |
| 34 | +### 2. Logger 实现 |
| 35 | + |
| 36 | +```typescript |
| 37 | +// src/logging/logger.ts |
| 38 | + |
| 39 | +import type { LogData, Logger, LoggerOptions, LogLevel } from './types' |
| 40 | +import { appendFileSync, existsSync, mkdirSync } from 'node:fs' |
| 41 | +import { dirname } from 'node:path' |
| 42 | + |
| 43 | +// 日志级别优先级 |
| 44 | +const LEVEL_ORDER: Record<LogLevel, number> = { |
| 45 | + debug: 0, |
| 46 | + info: 1, |
| 47 | + success: 1, |
| 48 | + warn: 2, |
| 49 | + error: 3 |
| 50 | +} |
| 51 | + |
| 52 | +// 终端颜色代码 |
| 53 | +const COLORS = { |
| 54 | + reset: '\x1B[0m', // 重置 |
| 55 | + dim: '\x1B[2m', // 暗淡(用于时间戳) |
| 56 | + debug: '\x1B[36m', // 青色 |
| 57 | + info: '\x1B[34m', // 蓝色 |
| 58 | + success: '\x1B[32m', // 绿色 |
| 59 | + warn: '\x1B[33m', // 黄色 |
| 60 | + error: '\x1B[31m' // 红色 |
| 61 | +} |
| 62 | + |
| 63 | +// 格式化时间戳 |
| 64 | +function formatTime(date: Date): string { |
| 65 | + const yyyy = date.getFullYear() |
| 66 | + const MM = String(date.getMonth() + 1).padStart(2, '0') |
| 67 | + const dd = String(date.getDate()).padStart(2, '0') |
| 68 | + const HH = String(date.getHours()).padStart(2, '0') |
| 69 | + const mm = String(date.getMinutes()).padStart(2, '0') |
| 70 | + const ss = String(date.getSeconds()).padStart(2, '0') |
| 71 | + return `${yyyy}-${MM}-${dd} ${HH}:${mm}:${ss}` |
| 72 | +} |
| 73 | + |
| 74 | +// 填充级别字符串到固定宽度 |
| 75 | +function padLevel(level: string): string { |
| 76 | + return level.padEnd(7) |
| 77 | +} |
| 78 | + |
| 79 | +// 创建日志函数 |
| 80 | +function createLogFunction( |
| 81 | + tag: string, |
| 82 | + level: LogLevel, |
| 83 | + currentLevelRef: { value: LogLevel }, |
| 84 | + filePath?: string |
| 85 | +): (message: string, data?: LogData | Error) => void { |
| 86 | + return (message: string, data?: LogData | Error) => { |
| 87 | + // 检查日志级别 |
| 88 | + if (LEVEL_ORDER[level] < LEVEL_ORDER[currentLevelRef.value]) { |
| 89 | + return |
| 90 | + } |
| 91 | + |
| 92 | + const timestamp = formatTime(new Date()) |
| 93 | + const tagStr = `[${tag}]` |
| 94 | + const levelStr = padLevel(level.toUpperCase()) |
| 95 | + const dataStr = data ? ` ${JSON.stringify(data)}` : '' |
| 96 | + |
| 97 | + // 控制台输出(带颜色) |
| 98 | + const color = COLORS[level] |
| 99 | + const consoleMsg = `${COLORS.dim}${timestamp}${COLORS.reset} ${color}${tagStr}${COLORS.reset} ${levelStr} ${message}${dataStr}` |
| 100 | + console.log(consoleMsg) |
| 101 | + |
| 102 | + // 文件输出(无颜色) |
| 103 | + if (filePath) { |
| 104 | + try { |
| 105 | + const dir = dirname(filePath) |
| 106 | + if (!existsSync(dir)) { |
| 107 | + mkdirSync(dir, { recursive: true }) |
| 108 | + } |
| 109 | + const fileMsg = `${timestamp} ${tagStr} ${levelStr} ${message}${dataStr}\n` |
| 110 | + appendFileSync(filePath, fileMsg, 'utf-8') |
| 111 | + } |
| 112 | + catch { |
| 113 | + // 静默失败 |
| 114 | + } |
| 115 | + } |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +// 创建日志器 |
| 120 | +export function createLogger(tag: string, options: LoggerOptions = {}): Logger { |
| 121 | + const { |
| 122 | + level = 'info', |
| 123 | + file, |
| 124 | + timestamp = true |
| 125 | + } = options |
| 126 | + |
| 127 | + const currentLevelRef = { value: level } |
| 128 | + |
| 129 | + return { |
| 130 | + debug: createLogFunction(tag, 'debug', currentLevelRef, file), |
| 131 | + info: createLogFunction(tag, 'info', currentLevelRef, file), |
| 132 | + success: createLogFunction(tag, 'success', currentLevelRef, file), |
| 133 | + warn: createLogFunction(tag, 'warn', currentLevelRef, file), |
| 134 | + error: createLogFunction(tag, 'error', currentLevelRef, file), |
| 135 | + setLevel(newLevel: LogLevel) { |
| 136 | + currentLevelRef.value = newLevel |
| 137 | + } |
| 138 | + } |
| 139 | +} |
| 140 | +``` |
| 141 | + |
| 142 | +### 3. 导出 |
| 143 | + |
| 144 | +```typescript |
| 145 | +// src/logging/index.ts |
| 146 | + |
| 147 | +export { createLogger } from './logger' |
| 148 | +export type { LogData, Logger, LoggerOptions, LogLevel } from './types' |
| 149 | +``` |
| 150 | + |
| 151 | +### 4. 主入口导出 |
| 152 | + |
| 153 | +```typescript |
| 154 | +// src/index.ts |
| 155 | + |
| 156 | +export * from './logging' |
| 157 | +// ... 其他导出 |
| 158 | +``` |
| 159 | + |
| 160 | +## 设计说明 |
| 161 | + |
| 162 | +### 颜色输出 |
| 163 | + |
| 164 | +使用 ANSI 转义码实现终端彩色输出: |
| 165 | + |
| 166 | +```typescript |
| 167 | +const COLORS = { |
| 168 | + reset: '\x1B[0m', // 重置 |
| 169 | + dim: '\x1B[2m', // 暗淡(用于时间戳) |
| 170 | + debug: '\x1B[36m', // 青色 |
| 171 | + info: '\x1B[34m', // 蓝色 |
| 172 | + success: '\x1B[32m', // 绿色 |
| 173 | + warn: '\x1B[33m', // 黄色 |
| 174 | + error: '\x1B[31m' // 红色 |
| 175 | +} |
| 176 | +``` |
| 177 | + |
| 178 | +### 日志级别过滤 |
| 179 | + |
| 180 | +使用数值比较实现日志级别过滤: |
| 181 | + |
| 182 | +```typescript |
| 183 | +const LEVEL_ORDER: Record<LogLevel, number> = { |
| 184 | + debug: 0, |
| 185 | + info: 1, |
| 186 | + success: 1, |
| 187 | + warn: 2, |
| 188 | + error: 3 |
| 189 | +} |
| 190 | + |
| 191 | +// 只有当当前级别 >= 设定级别时才输出 |
| 192 | +if (LEVEL_ORDER[level] < LEVEL_ORDER[currentLevelRef.value]) { |
| 193 | + // 跳过此日志 |
| 194 | +} |
| 195 | +``` |
| 196 | + |
| 197 | +### 文件输出 |
| 198 | + |
| 199 | +文件输出不包含 ANSI 颜色代码,便于日志解析: |
| 200 | + |
| 201 | +```typescript |
| 202 | +// 控制台:带颜色 |
| 203 | +const consoleMsg = `${COLORS.dim}${timestamp}${COLORS.reset} ...` |
| 204 | + |
| 205 | +// 文件:纯文本 |
| 206 | +const fileMsg = `${timestamp} ${tagStr} ${levelStr} ${message}${dataStr}\n` |
| 207 | +``` |
| 208 | + |
| 209 | +## 使用示例 |
| 210 | + |
| 211 | +### 为不同模块创建日志器 |
| 212 | + |
| 213 | +```typescript |
| 214 | +// 为不同模块创建日志器 |
| 215 | +import { createLogger } from 'frp-bridge' |
| 216 | + |
| 217 | +// src/rpc/logger.ts |
| 218 | +export const rpcLogger = createLogger('RPC', { |
| 219 | + file: './logs/rpc.log' |
| 220 | +}) |
| 221 | + |
| 222 | +// src/process/logger.ts |
| 223 | +export const processLogger = createLogger('Process', { |
| 224 | + file: './logs/process.log' |
| 225 | +}) |
| 226 | +``` |
| 227 | + |
| 228 | +### 运行时调整级别 |
| 229 | + |
| 230 | +```typescript |
| 231 | +const logger = createLogger('MyModule') |
| 232 | + |
| 233 | +// 生产环境只显示警告及以上 |
| 234 | +logger.setLevel('warn') |
| 235 | + |
| 236 | +// 开发时开启调试 |
| 237 | +logger.setLevel('debug') |
| 238 | +``` |
| 239 | + |
| 240 | +### 结构化数据 |
| 241 | + |
| 242 | +```typescript |
| 243 | +// 使用对象传递结构化数据 |
| 244 | +logger.info('User logged in', { |
| 245 | + userId: 123, |
| 246 | + username: 'alice', |
| 247 | + ip: '192.168.1.100' |
| 248 | +}) |
| 249 | +``` |
| 250 | + |
| 251 | +### 错误处理 |
| 252 | + |
| 253 | +```typescript |
| 254 | +// 记录 Error 对象 |
| 255 | +try { |
| 256 | + await someOperation() |
| 257 | +} |
| 258 | +catch (error) { |
| 259 | + logger.error('Operation failed', error) |
| 260 | +} |
| 261 | +``` |
| 262 | + |
| 263 | +## 输出格式 |
| 264 | + |
| 265 | +### 控制台输出(带颜色) |
| 266 | + |
| 267 | +``` |
| 268 | +2026-02-05 14:30:45 [RPC] INFO Server started {"port":7000} |
| 269 | +2026-02-05 14:30:46 [RPC] SUCCESS Connection established |
| 270 | +2026-02-05 14:30:47 [RPC] WARN High memory usage {"usage":"90%"} |
| 271 | +2026-02-05 14:30:48 [RPC] ERROR Connection failed |
| 272 | +``` |
| 273 | + |
| 274 | +### 文件输出(纯文本) |
| 275 | + |
| 276 | +``` |
| 277 | +2026-02-05 14:30:45 [RPC] INFO Server started {"port":7000} |
| 278 | +2026-02-05 14:30:46 [RPC] SUCCESS Connection established |
| 279 | +2026-02-05 14:30:47 [RPC] WARN High memory usage {"usage":"90%"} |
| 280 | +2026-02-05 14:30:48 [RPC] ERROR Connection failed |
| 281 | +``` |
| 282 | + |
| 283 | +## 最佳实践 |
| 284 | + |
| 285 | +### 1. 使用有意义的标签 |
| 286 | + |
| 287 | +```typescript |
| 288 | +// 好的标签 |
| 289 | +const apiLogger = createLogger('API') |
| 290 | +const dbLogger = createLogger('Database') |
| 291 | +const wsLogger = createLogger('WebSocket') |
| 292 | + |
| 293 | +// 不好的标签 |
| 294 | +const logger1 = createLogger('log1') |
| 295 | +const l1 = createLogger('logger') |
| 296 | +``` |
| 297 | + |
| 298 | +### 2. 为每个模块创建独立日志器 |
| 299 | + |
| 300 | +```typescript |
| 301 | +// src/rpc/index.ts |
| 302 | +export const logger = createLogger('RPC') |
| 303 | + |
| 304 | +// src/process/index.ts |
| 305 | +export const logger = createLogger('Process') |
| 306 | + |
| 307 | +// src/config/index.ts |
| 308 | +export const logger = createLogger('Config') |
| 309 | +``` |
| 310 | + |
| 311 | +### 3. 使用结构化数据 |
| 312 | + |
| 313 | +```typescript |
| 314 | +// 好的做法 |
| 315 | +logger.info('Tunnel added', { |
| 316 | + name: 'ssh', |
| 317 | + type: 'tcp', |
| 318 | + localPort: 22, |
| 319 | + remotePort: 6000 |
| 320 | +}) |
| 321 | + |
| 322 | +// 不好的做法 |
| 323 | +logger.info('Tunnel ssh tcp 22 6000 added') |
| 324 | +``` |
| 325 | + |
| 326 | +### 4. 正确的错误处理 |
| 327 | + |
| 328 | +```typescript |
| 329 | +// 好的做法 |
| 330 | +try { |
| 331 | + await connect() |
| 332 | +} |
| 333 | +catch (error) { |
| 334 | + logger.error('Connection failed', error) |
| 335 | +} |
| 336 | + |
| 337 | +// 不好的做法 |
| 338 | +try { |
| 339 | + await connect() |
| 340 | +} |
| 341 | +catch (error) { |
| 342 | + logger.error('Connection failed') // 丢失了错误信息 |
| 343 | +} |
| 344 | +``` |
| 345 | + |
| 346 | +## 环境变量配置 |
| 347 | + |
| 348 | +```typescript |
| 349 | +const logger = createLogger('Main', { |
| 350 | + level: (process.env.LOG_LEVEL as LogLevel) || 'info', |
| 351 | + file: process.env.LOG_FILE |
| 352 | +}) |
| 353 | +``` |
0 commit comments