轻松打造高效日志系统

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

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

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

  • 日志可以帮助我们找到影响最终用户的错误。
  • 日志可以跟踪系统的 "健康状况",在系统出问题之前察觉到某些 "异常迹象"。
  • ……等等

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

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

  • 了解在操作系统中记录日志的重要性。
  • 可以作为实施日志系统时的参考。

日志策略

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

  • Why(为什么):日志记录的目的是什么?
  • Who(谁): 哪个模块将生成日志?
  • When(何时):何时输出日志?
  • Where(哪里):在哪里输出日志(发送到 Slack 或 BigQuery 等)
  • What(什么):日志能提供什么信息?
  • How(如何): 如何输出日志?

日志级别

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

Log level Concept How to handle Example
FATAL This Level hinder the operating of the system Have to fix immediately Can not connect to the DB
ERROR Unexpected errors occur Should be fixed as soon as you can Can not send the email
WARN Not an error, but are some problems like unexpected input or unexpected executing unexpected input or unexpected executing Should be refactored regularly Regularly delete data API
INFO Notification when starting or ending an executing or a transaction. Maybe outputting another needed information Do not need to fix Output the body of the request or response
DEBUG The information that relating to system status Do not output in the production environment Can be put inside a function
TRACE Information that is more detailed than DEBUG Do not output in the production environment

案例

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

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

  • Why(为什么)
  • Who(谁)
  • When(何时)
  • Where(哪里)
  • What(什么)
  • How(如何)
系统日志(System Log)
  • Why: 当系统出现错误时,系统日志将用于调试。
  • Who: 系统本身将输出日志。
  • When:出错时输出日志。
  • Where:
    1. FATAL / ERROR:通知开发人员立即处理。
    2. WARN / INFO:在系统或日志管理工具中输出。
    3. DEBUG / TRACE:输出到预发环境中的 console.log
  • What:
    1. FATAL / ERROR:堆栈跟踪。
    2. WARN / INFO / DEBUG/ TRACE:要通知的内容。
  • How:
    1. FATAL / ERROR:通过日志管理工具或 Slack、SMS......(推模式)输出。
    2. 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 log
export 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 log
export 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",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

本文由mdnice多平台发布

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,692评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,482评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,995评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,223评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,245评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,208评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,091评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,929评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,346评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,570评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,739评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,437评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,037评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,677评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,833评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,760评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,647评论 2 354

推荐阅读更多精彩内容