You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 00:47:06