Express API中如何在服务与控制器间共享上下文实现规范日志
Express 控制器与服务层日志上下文及请求ID传递方案
一、日志上下文共享与错误时上下文传递
1. 定义可复用的日志上下文载体
创建包含日志实例和上下文数据的对象,在控制器初始化后传递给服务层,服务层可随时更新上下文数据,错误发生时能直接挂载到错误对象或请求对象上。
控制器示例:
// 定义日志上下文类型 type LogContext = { logger: typeof chLogger; contextData: Record<string, any>; }; const createComment = async (req: Request, res: Response, next: NextFunction) => { let logContext: LogContext; try { const { commentText, createdByUserId, postId, postUserId } = req.body; // 初始化基础日志实例 const baseLogger = logger.child(req, 'commentController.ts', 'createComment', { createdByUserId, postId, }); // 构建日志上下文 logContext = { logger: baseLogger, contextData: {} }; logContext.logger.debug('Creating a new comment'); // 传递上下文到服务层,接收返回的业务结果 const { newComment, userNotification } = await CommentService.create(logContext, commentText, createdByUserId, postId, postUserId); // 更新控制器侧的上下文数据 logContext.contextData.userNotification = userNotification; logContext.logger.debug('User notification created'); res.status(201).json(newComment); } catch (error) { // 将上下文附加到错误对象,供中间件读取 if (logContext) { (error as any).logContext = logContext; } next(error); } };
2. 服务层更新上下文并记录日志
服务层接收日志上下文对象,执行业务逻辑时更新上下文数据,同时使用上下文内的日志实例记录操作:
// CommentService.ts export class CommentService { static async create(logContext: LogContext, commentText: string, createdByUserId: string, postId: string, postUserId: string) { const newComment = await Comment.create(commentText, createdByUserId, postId); logContext.logger.verbose('Comment created', { comment: newComment }); let userNotification = null; if (newComment) { userNotification = await UserNotification.create(newComment.commentId, postId, 'comment', postUserId, createdByUserId); // 将生成的通知存入上下文 logContext.contextData.userNotification = userNotification; } return { newComment, userNotification }; } }
3. 错误中间件读取上下文记录日志
在全局错误处理中间件中,直接读取错误对象上的日志上下文,输出包含完整上下文的错误日志:
const errorHandler = (err: Error & { logContext?: LogContext }, req: Request, res: Response, next: NextFunction) => { const logContext = err.logContext; if (logContext) { logContext.logger.error('Request failed', { error: err.message, stack: err.stack, ...logContext.contextData }); } else { logger.error('Request failed without context', { error: err }); } res.status(500).json({ message: 'Internal server error' }); };
二、请求ID传递(避免显式传递req)
1. 使用Async Local Storage(Node.js 14.17+)
利用Node.js原生的AsyncLocalStorage在异步调用链中共享请求上下文,无需显式传递req或请求ID。
初始化上下文存储
// src/middleware/requestContext.ts import { AsyncLocalStorage } from 'async_hooks'; export const requestContext = new AsyncLocalStorage<{ requestId: string; logger: typeof logger }>();
中间件注入请求ID和基础日志
// src/middleware/requestIdMiddleware.ts import { requestContext } from './requestContext'; import { randomUUID } from 'crypto'; export const requestIdMiddleware = (req: Request, res: Response, next: NextFunction) => { // 从请求头获取或生成唯一请求ID const requestId = req.headers['x-request-id'] || randomUUID(); // 生成带请求ID的日志实例 const reqLogger = logger.child({ requestId }); // 启动上下文,后续所有异步操作可直接获取 requestContext.run({ requestId, logger: reqLogger }, () => { req.requestId = requestId; next(); }); };
控制器与服务层获取上下文
// 控制器中使用 const createComment = async (req: Request, res: Response, next: NextFunction) => { try { const { logger } = requestContext.getStore()!; const controllerLogger = logger.child({ module: 'commentController.ts', method: 'createComment' }); controllerLogger.debug('Starting comment creation'); const { commentText, createdByUserId, postId, postUserId } = req.body; const newComment = await CommentService.create(commentText, createdByUserId, postId, postUserId); res.status(201).json(newComment); } catch (err) { next(err); } }; // 服务层中使用 export class CommentService { static async create(commentText: string, createdByUserId: string, postId: string, postUserId: string) { const { logger } = requestContext.getStore()!; const serviceLogger = logger.child({ module: 'CommentService' }); serviceLogger.debug('Creating comment in service'); const newComment = await Comment.create(commentText, createdByUserId, postId); serviceLogger.verbose('Comment created', { comment: newComment }); return newComment; } }
2. 依赖注入(DI)容器方案
如果项目使用DI容器(如TypeDI、Awilix),可将请求级上下文注册为请求作用域服务,服务层通过依赖注入自动获取,无需显式传递。
TypeDI示例:
// 定义请求上下文类 class RequestContext { requestId: string; logger: typeof logger; constructor(requestId: string, logger: typeof logger) { this.requestId = requestId; this.logger = logger; } } // 中间件注册请求作用域实例 app.use((req, res, next) => { const requestId = req.headers['x-request-id'] || randomUUID(); const reqLogger = logger.child({ requestId }); Container.register(RequestContext, { useValue: new RequestContext(requestId, reqLogger), scope: TypeDI.Scope.Request // 每个请求生成新实例 }); next(); }); // 服务层注入上下文 @Service() export class CommentService { constructor(private readonly requestContext: RequestContext) {} async create(commentText: string, createdByUserId: string, postId: string, postUserId: string) { const serviceLogger = this.requestContext.logger.child({ module: 'CommentService' }); serviceLogger.debug('Creating comment'); const newComment = await Comment.create(commentText, createdByUserId, postId); return newComment; } }
总结
- 日志上下文:通过自定义上下文对象在控制器与服务间传递,服务层更新上下文数据,错误时挂载到错误对象供中间件使用,保证日志上下文的连贯性。
- 请求ID:优先使用Async Local Storage实现异步链上下文共享,或借助DI容器的请求作用域服务,彻底避免显式传递req的冗余操作。
内容的提问来源于stack exchange,提问作者Car Lord VR
相关产品推荐
相关产品推荐

