NestJS生产环境日志规范:用Winston打造企业级可观测性系统

📅 发布时间:2026/7/7 5:15:25 👁️ 浏览次数:
NestJS生产环境日志规范:用Winston打造企业级可观测性系统
NestJS生产环境日志规范用Winston打造企业级可观测性系统当你的NestJS应用从本地开发环境走向生产面对成百上千的并发请求和复杂的业务链路时最让你夜不能寐的是什么是某个深夜突发的线上故障却因为日志散乱、关键信息缺失导致团队花了三个小时才定位到问题根源。我经历过不止一次这样的时刻也正是这些教训让我意识到一个设计良好的日志系统不是可选项而是现代企业级应用的生存底线。今天我们不谈那些浅尝辄止的配置教程而是深入探讨如何基于Winston构建一套符合DevOps理念、具备完整可观测性的日志体系。这套体系要解决的不仅仅是“把日志打印出来”而是如何让日志成为你诊断问题、分析性能、理解用户行为的“数据雷达”。无论是微服务架构下的分布式追踪还是应对安全审计的敏感信息过滤或是满足合规要求的长期归档我们都需要一套深思熟虑的方案。1. 超越控制台构建分层的日志输出策略很多团队在日志处理上犯的第一个错误就是把所有日志都混在一起。开发时在控制台看到的五彩斑斓的输出直接照搬到生产环境结果就是信息过载和关键信号被淹没。一个成熟的日志系统首先要做的是分级和分流。在Winston的语境里这通过transports传输器来实现。每个传输器可以独立配置日志级别、输出目标和格式化方式。想象一下这样的场景info级别的常规操作日志写入按日滚动的文件便于日常巡检warn和error级别的异常日志除了写入文件还实时发送到Slack或钉钉群触发即时告警而debug日志只在特定调试环境下输出到控制台。下面是一个基础但完整的分层配置示例我们在src/config/winston.config.ts中定义import { WinstonModuleOptions } from nest-winston; import * as winston from winston; import winston-daily-rotate-file; import * as path from path; // 根据环境变量决定日志根目录 const logDir process.env.LOG_DIR || path.join(process.cwd(), logs); const environment process.env.NODE_ENV || development; export const winstonConfig: WinstonModuleOptions { // 定义日志级别顺序从最重要到最不重要 levels: winston.config.syslog.levels, // 可选使用syslog标准级别 format: winston.format.combine( winston.format.timestamp({ format: YYYY-MM-DD HH:mm:ss.SSS }), winston.format.errors({ stack: true }), // 捕获并格式化错误堆栈 winston.format.splat(), // 支持字符串插值 %s %d winston.format.json() // 最终输出为JSON便于后续解析 ), transports: [ // 传输器1: 控制台输出仅开发环境且美化格式 ...(environment development ? [ new winston.transports.Console({ level: debug, format: winston.format.combine( winston.format.colorize(), winston.format.printf(({ timestamp, level, message, context, trace }) { return ${timestamp} [${context || Application}] ${level}: ${message} ${trace || }; }) ), }), ] : []), // 传输器2: 按日滚动的通用日志文件记录info及以上级别 new winston.transports.DailyRotateFile({ level: info, dirname: path.join(logDir, application), filename: app-%DATE%.log, datePattern: YYYY-MM-DD, zippedArchive: true, // 归档时自动压缩为gzip maxSize: 50m, maxFiles: 30d, // 保留最近30天的日志 format: winston.format.json(), // 文件存储坚持用JSON }), // 传输器3: 独立的错误日志文件只记录warn和error new winston.transports.DailyRotateFile({ level: warn, // 会捕获warn, error, fatal dirname: path.join(logDir, error), filename: error-%DATE%.log, datePattern: YYYY-MM-DD, maxFiles: 90d, // 错误日志保留更久 auditFile: path.join(logDir, error-audit.json), // 审计文件记录滚动元数据 }), ], // 处理未捕获的Promise拒绝和异常 exceptionHandlers: [ new winston.transports.File({ filename: path.join(logDir, exceptions.log), maxsize: 10 * 1024 * 1024, // 10MB }) ], rejectionHandlers: [ new winston.transports.File({ filename: path.join(logDir, rejections.log) }) ], };注意生产环境务必关闭控制台输出。控制台I/O是同步阻塞的在高并发下会成为性能瓶颈且日志容易在进程重启时丢失。这个配置的核心思想是分离关注点。不同级别、不同用途的日志流向不同的目的地后续的监控、告警和分析系统可以各取所需。比如你的错误追踪系统如Sentry只需要订阅错误日志文件而业务分析团队可能更关心info级别的用户行为日志。1.1 日志级别的实战定义仅仅使用debug、info、error这些默认级别是不够的。我建议团队内部明确每个级别的具体含义形成约定级别使用场景是否告警存储策略error系统功能不可用、核心业务流程失败、数据库连接失败立即告警电话/短信长期存储90天同步至错误监控平台warn非核心功能异常、第三方API调用超时、资源使用率预警延迟告警企业微信/邮件中期存储30天每日汇总报告info业务流程关键节点、用户重要操作登录、支付、系统启动/关闭不告警短期存储7天用于业务审计和调试debug详细的函数入参/出参、复杂的中间状态、性能 profiling 数据不告警仅开发/测试环境开启生产环境按需动态开启verbose最细粒度的跟踪如每个中间件的执行、每个数据库查询语句不告警仅在本地调试时开启在代码中你应该这样使用它们export class PaymentService { constructor(private readonly logger: Logger) {} async processPayment(orderId: string, amount: number) { // 关键业务开始记录info this.logger.log(开始处理订单支付 [orderId${orderId}, amount${amount}], PaymentService); try { // 复杂的业务逻辑记录debug信息 this.logger.debug(调用风控检查参数: ${JSON.stringify({ orderId })}, PaymentService); const riskResult await this.riskService.check(orderId); if (!riskResult.passed) { // 业务规则拒绝记录warn不是系统错误但需要关注 this.logger.warn(订单风控检查未通过 [orderId${orderId}, reason${riskResult.reason}], PaymentService); throw new BusinessException(风控检查失败); } // 调用第三方支付网关 this.logger.debug(调用支付网关API开始, PaymentService); const paymentResult await this.paymentGateway.charge(amount); // 成功完成 this.logger.log(支付成功 [orderId${orderId}, transactionId${paymentResult.id}], PaymentService); return paymentResult; } catch (error) { // 真正的系统错误记录error包含完整堆栈 this.logger.error(支付处理失败 [orderId${orderId}], error.stack, PaymentService); // 根据错误类型可能还需要触发告警 if (error instanceof NetworkError) { this.logger.error(支付网关网络异常需要人工介入, null, PaymentService); // 这里可以集成告警逻辑 } throw error; } } }注意logger方法的第二个参数context这里是PaymentService。这看似是个小细节但在排查问题时能帮你快速定位日志来源尤其是在微服务架构下。2. 日志的自动化管理与生命周期日志文件不能无限增长。你需要一套自动化机制来管理日志的创建、滚动、归档和清理。winston-daily-rotate-file是这个领域的瑞士军刀但很多人只用了它最基本的功能。2.1 高级滚动策略与事件钩子除了按日期滚动我们经常需要根据文件大小、日志级别或业务模块进行更精细的控制。更重要的是当滚动事件发生时比如生成新文件、归档旧文件我们可能希望执行一些附加操作发送通知、更新监控指标、或将归档文件同步到云存储。import * as DailyRotateFile from winston-daily-rotate-file; import * as fs from fs; import * as path from path; // 创建业务特定的日志传输器 function createBusinessTransport(moduleName: string, level: string) { const transport new DailyRotateFile({ level: level, dirname: path.join(logDir, business, moduleName), filename: ${moduleName}-%DATE%.log, datePattern: YYYY-MM-DD, maxSize: 100m, maxFiles: 14d, zippedArchive: true, auditFile: path.join(logDir, audit, ${moduleName}-audit.json), // 事件监听器 - 这是关键 listeners: { // 新日志文件创建时每天第一次写入或文件大小达到上限时 new: (newFilename) { console.log(新的日志文件创建: ${newFilename}); // 可以在这里发送指标到监控系统 // statsd.increment(logfile.created, 1, { module: moduleName }); }, // 日志文件滚动归档时 rotated: (oldFilename, newFilename) { console.log(日志文件已滚动: ${oldFilename} - ${newFilename}); // 示例将归档的日志文件上传到云存储如AWS S3 // 这对于合规性要求长期保存日志的场景至关重要 // uploadToCloudStorage(oldFilename).catch(err { // console.error(上传日志文件失败: ${err.message}); // }); }, // 日志文件因超过保留期限而被删除时 archive: (zipFilename) { console.log(日志文件已压缩归档: ${zipFilename}); // 可以记录审计日志或更新数据库 }, // 错误事件 error: (error) { console.error(DailyRotateFile错误: ${error.message}); // 这里应该触发告警日志系统自身不能静默失败 // alertService.send(日志系统异常, error.message); } } }); return transport; } // 在主配置中使用 export const winstonConfig: WinstonModuleOptions { // ... 其他配置 transports: [ // ... 基础传输器 createBusinessTransport(payment, info), createBusinessTransport(user, info), createBusinessTransport(notification, debug), ], };提示auditFile参数非常重要。它存储了日志滚动的元数据如当前文件大小、已滚动文件列表确保即使在应用重启后滚动逻辑也能正确继续避免日志丢失或重复。2.2 基于日志内容的动态归档有时候简单的按日期/大小滚动还不够。比如你可能希望将所有包含特定错误码的日志单独归档或者将高价值用户的请求日志永久保存。这需要结合自定义格式化和传输器逻辑import { TransformableInfo } from logform; // 自定义传输器将包含特定用户ID的日志单独存储 class UserSpecificTransport extends winston.transports.File { private targetUserIds: string[]; constructor(userIds: string[], options: winston.transport.TransportStreamOptions) { super(options); this.targetUserIds userIds; } log(info: TransformableInfo, callback: Function) { // 检查日志中是否包含目标用户ID const userId info.meta?.userId || info.message?.match(/userId([^,\s])/)?.[1]; if (userId this.targetUserIds.includes(userId)) { // 符合条件调用父类方法写入 super.log(info, callback); } else { // 不符合条件跳过 callback(); } } } // 或者更灵活的方式使用winston的过滤功能 const vipUserFilter winston.format((info, opts) { const vipUsers [user123, user456, user789]; const userId info.meta?.userId; // 如果不是VIP用户的日志直接丢弃不传递给下一个传输器 if (userId !vipUsers.includes(userId)) { return false; } return info; }); // 在传输器中使用这个过滤器 const vipTransport new winston.transports.File({ filename: vip-users.log, format: winston.format.combine( vipUserFilter(), winston.format.json() ), maxsize: 10 * 1024 * 1024, // 10MB });这种基于内容的动态路由在排查特定用户问题或满足合规审计要求时非常有用。你可以轻松地说“把用户A在过去一周的所有操作日志都找出来”而不需要从海量日志中手动筛选。3. 企业级日志增强上下文、追踪与安全基础的日志记录只是第一步。在生产环境中你需要为每一条日志注入丰富的上下文信息让它们不再是孤立的文本行而是可关联、可追溯的数据点。3.1 分布式追踪ID注入在微服务架构中一个用户请求可能经过网关、认证服务、订单服务、支付服务等多个模块。如果没有统一的追踪ID你就像在黑暗的迷宫里寻找线索。我们需要为每个请求分配唯一的traceId并让它贯穿整个调用链。首先创建一个全局的请求上下文中间件// src/middlewares/request-context.middleware.ts import { Injectable, NestMiddleware, Logger } from nestjs/common; import { Request, Response, NextFunction } from express; import { v4 as uuidv4 } from uuid; import { ClsService } from nestjs-cls; // 需要安装nestjs-cls Injectable() export class RequestContextMiddleware implements NestMiddleware { private readonly logger new Logger(RequestContextMiddleware.name); constructor(private readonly clsService: ClsService) {} use(req: Request, res: Response, next: NextFunction) { // 生成或获取追踪ID const traceId req.headers[x-trace-id]?.toString() || uuidv4(); const spanId uuidv4(); // 当前服务的span ID // 设置响应头便于前端调试 res.setHeader(X-Trace-Id, traceId); // 使用CLS连续本地存储保存上下文 this.clsService.run(() { this.clsService.set(traceId, traceId); this.clsService.set(spanId, spanId); this.clsService.set(userId, req.user?.id || anonymous); this.clsService.set(userAgent, req.headers[user-agent]); this.clsService.set(ip, req.ip); // 记录请求开始 this.logger.debug(请求开始 [method${req.method}, path${req.path}, traceId${traceId}]); // 监听请求完成 res.on(finish, () { const duration Date.now() - this.clsService.get(requestStartTime); this.logger.log(请求完成 [status${res.statusCode}, duration${duration}ms, traceId${traceId}]); }); next(); }); } }然后创建一个自定义的Winston格式化器自动从上下文中注入这些信息// src/utils/trace-id.formatter.ts import { format } from winston; import { ClsService } from nestjs-cls; export const traceIdFormat (clsService: ClsService) format((info) { // 从CLS中获取上下文信息 const traceId clsService.get(traceId); const spanId clsService.get(spanId); const userId clsService.get(userId); if (traceId) { info.traceId traceId; info.spanId spanId; info.userId userId; // 如果是错误添加更多上下文 if (info.level error info.stack) { info.service process.env.SERVICE_NAME || unknown; info.environment process.env.NODE_ENV || development; info.hostname require(os).hostname(); } } return info; });在Winston配置中使用这个格式化器// 在winston配置中 import { traceIdFormat } from ./trace-id.formatter; import { ClsService } from nestjs-cls; // 假设clsService已通过依赖注入获得 const clsService app.get(ClsService); export const winstonConfig: WinstonModuleOptions { format: winston.format.combine( winston.format.timestamp(), traceIdFormat(clsService)(), // 注入追踪ID winston.format.errors({ stack: true }), winston.format.json() ), // ... 其他配置 };现在每条日志都会自动包含traceId、userId等上下文信息。当你在ELK或类似的日志聚合系统中搜索时只需要一个traceId就能看到这个请求在所有服务中的所有相关日志就像看一部完整的电影而不是一堆零散的剧照。3.2 敏感信息过滤与数据脱敏日志中经常包含敏感信息用户密码、身份证号、银行卡号、API密钥等。这些信息如果明文写入日志不仅违反GDPR等数据保护法规还可能被内部人员滥用。必须在日志写入前进行脱敏。创建一个安全的格式化器// src/utils/sensitive-data.formatter.ts import { format } from winston; // 需要脱敏的字段模式 const SENSITIVE_PATTERNS [ // 密码字段 /password:\s*([^]*)/gi, /password([^\s])/gi, // 身份证号 /(\d{6})\d{8}(\d{4})/g, // 保留前6位和后4位 // 银行卡号 /(\d{4})\d{8,12}(\d{4})/g, // 保留前4位和后4位 // JWT令牌 /Bearer\s([a-zA-Z0-9\-_]\.[a-zA-Z0-9\-_]\.[a-zA-Z0-9\-_])/gi, // API密钥 /(api[_-]?key|apikey|access[_-]?token)\s*[:]\s*[]?([a-zA-Z0-9]{20,})[]?/gi, ]; // 脱敏函数 function maskSensitiveData(text: string): string { if (typeof text ! string) return text; let masked text; SENSITIVE_PATTERNS.forEach(pattern { if (pattern.source.includes(password)) { masked masked.replace(pattern, password: ***); } else if (pattern.source.includes(身份证)) { masked masked.replace(pattern, $1********$2); } else if (pattern.source.includes(银行卡)) { masked masked.replace(pattern, $1************$2); } else if (pattern.source.includes(Bearer)) { masked masked.replace(pattern, Bearer ***); } else if (pattern.source.includes(api)) { masked masked.replace(pattern, $1: ***); } }); return masked; } // Winston格式化器 export const sensitiveDataFormat format((info) { // 处理message字段 if (info.message typeof info.message string) { info.message maskSensitiveData(info.message); } // 处理meta字段如果有 if (info.meta typeof info.meta object) { // 深度遍历对象脱敏敏感字段 const maskObject (obj: any): any { if (!obj || typeof obj ! object) return obj; if (Array.isArray(obj)) { return obj.map(maskObject); } const masked { ...obj }; Object.keys(masked).forEach(key { const value masked[key]; // 检查键名是否包含敏感词 const isSensitiveKey /(password|passwd|pwd|secret|token|key|auth|credential|身份证|银行卡|身份证号)/i.test(key); if (isSensitiveKey typeof value string) { masked[key] ***; } else if (typeof value string) { // 即使键名不敏感值也可能包含敏感信息 masked[key] maskSensitiveData(value); } else if (value typeof value object) { masked[key] maskObject(value); } }); return masked; }; info.meta maskObject(info.meta); } return info; });在配置中这个格式化器应该放在JSON格式化之前export const winstonConfig: WinstonModuleOptions { format: winston.format.combine( winston.format.timestamp(), sensitiveDataFormat(), // 先脱敏 traceIdFormat(clsService)(), // 再注入追踪ID winston.format.json() // 最后序列化为JSON ), // ... 其他配置 };重要提醒脱敏规则需要根据具体业务定制并且要定期审查。有些场景下你可能需要完全禁止某些字段被记录而不是仅仅脱敏。另外考虑使用性能更好的正则表达式或者对于超大日志消息采用流式处理。4. 与可观测性生态集成日志不是孤立的。在现代DevOps实践中日志需要与指标Metrics、追踪Tracing一起构成完整的可观测性体系。Winston的强大之处在于它的可扩展性可以轻松集成到各种监控平台。4.1 集成ELK/EFK堆栈ELKElasticsearch, Logstash, Kibana或EFKElasticsearch, Fluentd, Kibana是最常见的日志聚合方案。要让Winston日志直接进入Elasticsearch你可以使用winston-elasticsearch传输器import { ElasticsearchTransport } from winston-elasticsearch; const esTransport new ElasticsearchTransport({ level: info, index: app-logs-${process.env.NODE_ENV}-${new Date().toISOString().split(T)[0]}, // 按日分索引 clientOpts: { node: process.env.ELASTICSEARCH_URL || http://localhost:9200, auth: { username: process.env.ELASTICSEARCH_USERNAME, password: process.env.ELASTICSEARCH_PASSWORD, }, ssl: { rejectUnauthorized: false, // 生产环境应该为true并配置CA }, }, // 确保日志格式与Elasticsearch映射兼容 mappingTemplate: { index_patterns: [app-logs-*], template: { settings: { number_of_shards: 1, number_of_replicas: 0, }, mappings: { properties: { timestamp: { type: date }, level: { type: keyword }, message: { type: text }, traceId: { type: keyword }, spanId: { type: keyword }, userId: { type: keyword }, service: { type: keyword }, environment: { type: keyword }, // 错误相关字段 stack: { type: text }, // 嵌套的meta字段 meta: { type: object, dynamic: true, }, }, }, }, }, // 缓冲选项提高性能 bufferLimit: 100, // 缓冲100条日志后批量发送 flushInterval: 5000, // 每5秒强制刷新一次 }); // 添加到Winston配置 transports: [ // ... 其他传输器 esTransport, ];为了处理网络波动或Elasticsearch不可用的情况建议实现一个降级策略class ResilientElasticsearchTransport extends ElasticsearchTransport { private localBuffer: any[] []; private maxBufferSize 1000; private isElasticsearchAvailable true; constructor(options: any) { super(options); // 定期检查ES可用性并重试发送缓冲的日志 setInterval(() { if (this.localBuffer.length 0 this.isElasticsearchAvailable) { this.flushBuffer(); } }, 30000); // 每30秒尝试一次 } log(info: any, callback: Function) { if (this.isElasticsearchAvailable) { // ES可用直接发送 super.log(info, (err: any) { if (err) { console.error(Elasticsearch发送失败降级到本地缓冲, err); this.isElasticsearchAvailable false; this.addToBuffer(info); } callback(); }); } else { // ES不可用缓冲到本地 this.addToBuffer(info); callback(); } } private addToBuffer(info: any) { this.localBuffer.push(info); // 如果缓冲超过上限移除最旧的日志 if (this.localBuffer.length this.maxBufferSize) { this.localBuffer.shift(); console.warn(日志缓冲达到上限丢弃最旧的日志条目); } // 尝试写入本地文件作为备份 try { const fs require(fs); const path require(path); const backupFile path.join(logDir, es-backup.log); fs.appendFileSync(backupFile, JSON.stringify(info) \n); } catch (err) { console.error(无法写入本地备份文件, err); } } private async flushBuffer() { if (this.localBuffer.length 0) return; const bufferCopy [...this.localBuffer]; this.localBuffer []; try { // 尝试批量发送所有缓冲的日志 for (const info of bufferCopy) { await new Promise((resolve, reject) { super.log(info, (err: any) { if (err) reject(err); else resolve(true); }); }); } this.isElasticsearchAvailable true; console.log(成功恢复并发送了${bufferCopy.length}条缓冲日志到Elasticsearch); } catch (err) { // 发送失败重新放回缓冲但可能丢失一部分 console.error(恢复发送失败重新缓冲日志, err); this.localBuffer [...bufferCopy, ...this.localBuffer].slice(0, this.maxBufferSize); } } }4.2 基于日志的告警触发日志不仅是事后分析的素材还可以实时触发告警。你可以结合Winston的事件系统和外部监控工具// src/monitoring/log-monitor.service.ts import { Injectable, OnModuleInit } from nestjs/common; import { EventEmitter } from events; import * as winston from winston; Injectable() export class LogMonitorService implements OnModuleInit { private alertRules [ { level: error, pattern: /(数据库连接失败|内存溢出|进程崩溃)/i, action: critical, // 紧急告警 cooldown: 300000, // 5分钟内不重复告警 }, { level: warn, pattern: /(响应时间超过\d秒|API调用失败|队列积压)/i, action: warning, // 警告 cooldown: 600000, // 10分钟内不重复告警 }, { level: error, pattern: /支付失败.*userIdVIP\d/i, // VIP用户支付失败 action: critical, cooldown: 0, // 每次都告警 }, ]; private lastAlertTime: Mapstring, number new Map(); constructor(private readonly logger: winston.Logger) {} onModuleInit() { // 监听所有日志事件 this.logger.on(logged, (info) { this.evaluateAlertRules(info); }); // 监听错误日志特别事件 this.logger.on(error, (error) { console.error(Winston自身发生错误:, error); // 这里应该触发更高级别的告警 }); } private evaluateAlertRules(info: any) { const now Date.now(); for (const rule of this.alertRules) { // 检查日志级别 if (info.level ! rule.level) continue; // 检查模式匹配 const message info.message || ; if (!rule.pattern.test(message)) continue; // 检查冷却时间 const ruleKey ${rule.level}-${rule.pattern.source}; const lastTime this.lastAlertTime.get(ruleKey) || 0; if (now - lastTime rule.cooldown) { continue; // 还在冷却期内 } // 触发告警 this.triggerAlert(rule, info); this.lastAlertTime.set(ruleKey, now); } } private async triggerAlert(rule: any, logInfo: any) { const alertMessage 日志告警 [${rule.action.toUpperCase()}]: ${logInfo.message} 时间: ${new Date(logInfo.timestamp).toLocaleString()} 服务: ${logInfo.service || unknown} 环境: ${logInfo.environment || unknown} Trace ID: ${logInfo.traceId || N/A} 用户ID: ${logInfo.userId || N/A}; // 根据告警级别采取不同行动 switch (rule.action) { case critical: // 紧急告警电话/短信 await this.sendSmsAlert(alertMessage); await this.sendToPagerDuty(alertMessage); break; case warning: // 警告企业微信/钉钉/Slack await this.sendChatAlert(alertMessage); break; } // 同时记录到专门的告警日志 this.logger.warn(告警已触发: ${rule.pattern.source}, { ...logInfo, alertRule: rule, alertTime: new Date().toISOString(), }); } private async sendSmsAlert(message: string) { // 集成短信网关API console.log(发送短信告警:, message); } private async sendChatAlert(message: string) { // 集成企业微信/钉钉机器人 console.log(发送聊天工具告警:, message); } private async sendToPagerDuty(message: string) { // 集成PagerDuty等值班系统 console.log(发送到值班系统:, message); } }这种基于规则的实时告警让你能在用户发现问题之前就采取行动。我曾经用类似的系统在数据库连接池开始出现异常时就收到告警避免了后续的大规模服务中断。4.3 性能考量与最佳实践日志记录不是免费的。不当的日志实践会显著影响应用性能。以下是一些关键的性能优化点异步日志记录Winston默认是异步的但某些传输器如控制台可能是同步的。生产环境一定要确保所有传输器都是异步的。批量写入对于文件或网络传输批量写入比逐条写入效率高得多。winston-elasticsearch等传输器内置了缓冲机制。采样率控制对于极高频率的debug日志可以考虑采样const samplingFormat winston.format((info, opts) { // 对debug日志进行采样只记录10% if (info.level debug Math.random() 0.1) { return false; // 丢弃 } return info; }); // 或者更智能的采样基于traceId const smartSamplingFormat winston.format((info, opts) { if (info.level debug info.traceId) { // 对同一个traceId的debug日志要么全记录要么全不记录 // 使用traceId的哈希值决定 const hash info.traceId.split().reduce((a, b) { a ((a 5) - a) b.charCodeAt(0); return a a; }, 0); if (hash % 10 ! 0) { // 只记录10%的trace return false; } } return info; });结构化日志的性能优势使用JSON等结构化格式虽然单条日志体积可能稍大但后续的解析、索引、查询效率远高于非结构化文本。Elasticsearch对JSON的支持也更好。日志级别的动态调整不需要重启应用就能调整日志级别// src/utils/log-level-manager.ts import { Injectable } from nestjs/common; import * as winston from winston; Injectable() export class LogLevelManager { private logger: winston.Logger; constructor() { // 从配置中心或环境变量获取初始级别 this.initialize(); } private initialize() { this.logger winston.createLogger({ // ... 配置 }); // 监听配置变化 setInterval(() this.checkForLevelUpdates(), 60000); // 每分钟检查一次 } private async checkForLevelUpdates() { try { // 从配置中心如Consul, etcd, Apollo获取最新日志级别配置 const newLevel await this.fetchLogLevelFromConfigCenter(); if (newLevel newLevel ! this.logger.level) { console.log(更新日志级别: ${this.logger.level} - ${newLevel}); this.logger.level newLevel; // 更新所有传输器的级别 this.logger.transports.forEach(transport { if (transport.setLevel) { transport.setLevel(newLevel); } }); } } catch (error) { this.logger.error(获取日志级别配置失败, error); } } // 动态为特定模块或用户开启详细日志 enableDebugForTrace(traceId: string, durationMinutes: number 30) { const debugTraces this.getDebugTraces(); debugTraces[traceId] Date.now() durationMinutes * 60 * 1000; this.saveDebugTraces(debugTraces); } // 在格式化器中检查 shouldLogDebug(info: any): boolean { if (info.level ! debug) return true; const debugTraces this.getDebugTraces(); const traceId info.traceId; if (traceId debugTraces[traceId]) { if (Date.now() debugTraces[traceId]) { return true; // 在调试期内记录debug } else { delete debugTraces[traceId]; // 过期清理 this.saveDebugTraces(debugTraces); } } return this.logger.level debug; // 否则遵循全局级别 } }这套日志系统在我们团队的生产环境中运行了两年多处理着每天数TB的日志数据。最让我自豪的不是它的技术复杂度而是它真正帮助团队减少了故障排查时间。现在当监控系统告警时我们第一反应不是慌张而是说“先看看日志traceId是多少”