写点什么

轻松打造高效日志系统

作者:俞凡
  • 2024-12-30
    上海
  • 本文字数:4559 字

    阅读完需:约 15 分钟

本文介绍了如何设计并实现高效日志系统,介绍了一个有效的日志系统需要考虑的关键问题,强调了日志在系统调试和监控中的重要性。原文:Design And Building A Logging System



作为开发者,经常需要在调试时查看检查日志,缺乏日志或者不清楚如何通过日志分析问题,就无法定位出错的代码。


对于每天为成千上万甚至上百万用户提供服务的系统来说,日志必不可少,因为:


  • 日志可以帮助我们找到影响最终用户的错误。

  • 日志可以跟踪系统的 "健康状况",在系统出问题之前察觉到某些 "异常迹象"。

  • ……等等


由此可见,在开发或运行系统时,日志至关重要,因此,设计和实施完善的日志系统有助于简化监控工作。


本文将分享我在设计和构建日志系统方面的经验和理解。希望通过这篇文章,你能:


  • 了解在操作系统中记录日志的重要性。

  • 可以作为实施日志系统时的参考。

日志策略

下面列出了我们在实施日志系统之前应该问自己的问题。


  • Why(为什么):日志记录的目的是什么?

  • Who(谁): 哪个模块将生成日志?

  • When(何时):何时输出日志?

  • Where(哪里):在哪里输出日志(发送到 Slack 或 BigQuery 等)

  • What(什么):日志能提供什么信息?

  • How(如何): 如何输出日志?

日志级别

了解日志的目的后,应该对日志进行分级


案例

定义日志级别后,必须明确要输出的日志类型。


本节将针对每种日志类型回答以下六个问题。


  • Why(为什么)

  • Who(谁)

  • When(何时)

  • Where(哪里)

  • What(什么)

  • How(如何)

系统日志(System Log)
  • Why: 当系统出现错误时,系统日志将用于调试。

  • Who: 系统本身将输出日志。

  • When:出错时输出日志。

  • Where:

  • FATAL / ERROR:通知开发人员立即处理。

  • WARN / INFO:在系统或日志管理工具中输出。

  • DEBUG / TRACE:输出到预发环境中的 console.log

  • What:

  • FATAL / ERROR:堆栈跟踪。

  • WARN / INFO / DEBUG/ TRACE:要通知的内容。

  • How:

  • FATAL / ERROR:通过日志管理工具或 Slack、SMS......(推模式)输出。

  • WARN / INFO / DEBUG / TRACE:通过日志管理工具或系统内部输出(拉模式)。

访问日志(Access Log)
  • Why: 输出日志以跟踪发送和接收请求的过程。

  • Who: 系统本身或基础设施。

  • When: 在发送或接收请求时输出。

  • Where: 在 INFO 级别和拉模式中。由于日志量可能很大,必须注意查找日志的速度。

  • What: 输出谁、如何、何时进入系统。

  • How: 根据目的不同,可能会有一些差异。

操作日志(Action Log)
  • Why: 分析用户操作,从而在此基础上改进服务。

  • Who: 系统本身或外部工具。

  • When: 某些操作发生时。

  • Where: 日志分析工具(BigQuery 等)。

  • What: 取决于目的。

  • How: 根据目的不同,可能会有一些差异。

认证日志(Auth Log)
  • Why: 跟踪用户验证的输出。

  • Who: 系统本身。

  • When: 验证用户。

  • Where: 在 INFO 级别和拉模式中。

  • What: 输出认证的时间、用户、方式。

  • How: 根据认证方法不同,可能会有一些差异。

示例

概念就介绍到这里,下面来看一个示例项目。


有关代码的更多详情,请参阅Github

选择日志库

我选择 log4js 库,原因很简单,因为 log4js 构建日志级别的方式与我的想法一致。

实施

步骤 1 - 定义日志类


首先定义日志类:



class Logger { public default: log4js.Logger; public system: log4js.Logger; public api: log4js.Logger; public access_req: log4js.Logger; public access_res: log4js.Logger; public sql: log4js.Logger; public auth: log4js.Logger;
public fatal: log4js.Logger; public error: log4js.Logger; public warn: log4js.Logger; public info: log4js.Logger; public debug: log4js.Logger; public trace: log4js.Logger;
constructor() { log4js.configure(loggerConfig);
this.system = log4js.getLogger('system'); this.api = log4js.getLogger('api'); this.access_req = log4js.getLogger('access_req'); this.access_res = log4js.getLogger('access_res'); this.sql = log4js.getLogger('sql'); this.auth = log4js.getLogger('auth');
this.fatal = log4js.getLogger('fatal'); this.fatal.level = log4js.levels.FATAL;
this.error = log4js.getLogger('error'); this.error.level = log4js.levels.ERROR;
this.warn = log4js.getLogger('warn'); this.warn.level = log4js.levels.WARN;
this.info = log4js.getLogger('info'); this.info.level = log4js.levels.INFO;
this.debug = log4js.getLogger('debug'); this.debug.level = log4js.levels.DEBUG;
this.trace = log4js.getLogger('trace'); this.trace.level = log4js.levels.TRACE; }}
复制代码


Logger 类中定义了日志级别:


  • fatal

  • error

  • warn

  • info

  • debug

  • trace


基于此,我又定义了日志类型:


  • system

  • api

  • access_req

  • access_res

  • sql

  • auth


第 2 步 - 将 Logger 应用到项目中


Logger 类应用到由 NestJS 框架实现的项目中。


通过 NestJS 的 Interceptor拦截器)功能,将日志类注入到项目中。


选择 Interceptor 的原因是 NestJS 拦截器不仅能封装请求流,还能封装从 API 输入和输出的响应流,因此使用拦截器是捕获请求日志和响应日志的最简单方法。我是这样定义 LoggerInterceptor 类的:


export class LoggerInterceptor implements NestInterceptor {  intercept(    context: ExecutionContext,    next: CallHandler<any>  ): Observable<any> | Promise<Observable<any>> {    // intercept() method will "wrap" request/ response stream
/* * Get request object from context * After that, pass request object to "requestLogger" function * to output the log */ const request = context.switchToHttp().getRequest(); requestLogger(request);
/* * Get response object from context * After that pass response object to "responseLogger" & "responseErrorLogger" functions for ouputting the log or * error log */ const response = context.switchToHttp().getResponse();
return next.handle().pipe( // 200 - Success Response map((data) => { responseLogger({ requestId: request._id, response, data }); }), // 4xx, 5xx - Error Response tap(null, (exception: HttpException | Error) => { try { responseErrorLogger({ requestId: request._id, exception }); } catch (e) { logger.access_res.error(e); } }) ); }}
复制代码


定义了三种方法:


  • requestLogger: 用于记录请求信息。

  • responseLogger: 用于记录响应信息。

  • responseErrorLogger: 用于记录错误信息。


像这样:



const MaskField = { Email: 'email', Password: 'password',} as const;
type MaskField = (typeof MaskField)[keyof typeof MaskField];
const _maskFields = (object: FixType, fields: MaskField[]): FixType => { const maskOptions = { maskWith: '*', unmaskedStartCharacters: 0, unmaskedEndCharacters: 0, };
for (let i = 0; i < fields.length; i++) { switch (fields[i]) { case MaskField.Email: { object[MaskField.Email] = maskData.maskEmail2( object[MaskField.Email], maskOptions ); } case MaskField.Password: { object[MaskField.Password] = maskData.maskPassword( object[MaskField.Password], maskOptions ); } } }
return object;};
export const requestLogger = (request: Request) => { const { ip, originalUrl, method, params, query, body, headers } = request;
// logTemplate includes: now(time), ip, http_method, url, request_object const logTemplate = '%s %s %s %s %s'; const now = dayjs().format('YYYY-MM-DD HH:mm:ss.SSS');
const logContent = util.formatWithOptions( { colors: true }, logTemplate, now, ip, method, originalUrl, JSON.stringify({ method, url: originalUrl, userAgent: headers['user-agent'], body: _maskFields(body, [MaskField.Email, MaskField.Password]), params, query, }) );
// Using access_req logger object have been defined before. logger.access_req.info(logContent);};
// Ouptput success response logexport const responseLogger = (input: { requestId: number; response: Response; data: any;}) => { const { requestId, response, data } = input;
const log: ResponseLog = { requestId, statusCode: response.statusCode, data, };
// Using access_res logger object have been defined before. logger.access_res.info(JSON.stringify(log));};
// Ouptput error response logexport const responseErrorLogger = (input: { requestId: number; exception: HttpException | Error;}) => { const { requestId, exception } = input;
const log: ResponseLog = { requestId, statusCode: exception instanceof HttpException ? exception.getStatus() : null, message: exception?.stack || exception?.message, };
// Using access_res logger object have been defined before. logger.access_res.info(JSON.stringify(log)); logger.access_res.error(exception);};
复制代码


定义完 LoggerInterceptor 后,将此拦截器应用到应用程序中:


const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggerInterceptor());
复制代码


在 NestJS 应用程序中应用自定义拦截器并不难,因为这是 NestJS 的内置功能。


对于 fataldebug 日志,我将在用例层或基础架构层中使用,以达到以下目的:


  • 通知无法连接数据库等致命错误。

  • 当用户遇到问题时进行调试。


只要这样做:


logger.fatal.error('Error message');
复制代码


可以将 fatal 日志输出到控制台或 Slack 等通知管道......


结果如下:


首先是访问请求日志和响应日志(当没有发生错误时)。



可以看到,与请求相关的信息,如 methodbody 等都已清晰显示。


如果出错:



同时显示错误类型和错误信息。


fatal 日志会是这样的:



同样会输出错误信息和错误类型。

结论

本文分享了如何设计和实施一个基本的日志系统。


通过简单的示例,希望你能理解建立日志系统的重要性和必要性,这将有助于系统的运行和调试。




你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

发布于: 刚刚阅读数: 4
用户头像

俞凡

关注

公众号:DeepNoMind 2017-10-18 加入

俞凡,Mavenir Systems研发总监,关注高可用架构、高性能服务、5G、人工智能、区块链、DevOps、Agile等。公众号:DeepNoMind

评论

发布
暂无评论
轻松打造高效日志系统_架构_俞凡_InfoQ写作社区