基于Egg框架的日记链路追踪实践分享

疾速导航

需求背景

完成全链路日记追踪,便于日记监控、题目排查、接口相应耗时数据统计等,起首 API 接口效劳接收到挪用方要求,依据挪用方传的 traceId,在该次挪用链中处置惩罚营业时,如需打印日记的,日记信息根据商定的范例举行打印,并纪录 traceId,完成日记链路追踪。

  • 日记途径商定
/var/logs/${projectName}/bizLog/${projectName}-yyyyMMdd.log
  • 日记花样商定
日记时候[]traceId[]效劳端IP[]客户端IP[]日记级别[]日记内容

采纳 Egg.js 框架 egg-logger 中间件,在完成过程当中发明关于根据以上日记花样打印是没法满足需求的(最少现在我还没找到可完成体式格局),假如要自身完成,可能要自身造轮子了,幸亏官方的 egg-logger 中间件供应了自定义日记扩大功用,参考 高等自定义日记,自身也供应了日记支解、多历程日记处置惩罚等功用。

egg-logger 供应了多种传输通道,我们的需求主如果对要求的营业日记自定义花样存储,重要用到 fileTransport 和 consoleTransport 两个通道,离别打印日记到文件和终端。

自定义日记插件开辟

基于 egg-logger 定制开辟一个插件项目,参考 插件开辟,以下以 egg-logger-custom 为项目,展现中间代码编写

  • 编写logger.js

egg-logger-custom/lib/logger.js

const moment = require('moment');
const FileTransport = require('egg-logger').FileTransport;
const utils = require('./utils');
const util = require('util');

/**
 * 继续 FileTransport
 */
class AppTransport extends FileTransport {
    constructor(options, ctx) {
        super(options);

        this.ctx = ctx; // 获得每次要求的上下文
    }

    log(level, args, meta) {
        // 猎取自定义花样音讯
        const customMsg = this.messageFormat({
            level,
        });

        // 针对 Error 音讯打印出毛病的客栈
        if (args[0] instanceof Error) {
            const err = args[0] || {};
            args[0] = util.format('%s: %s\n%s\npid: %s\n', err.name, err.message, err.stack, process.pid);
        } else {
            args[0] = util.format(customMsg, args[0]);
        }

        // 这个是必需的,不然日记文件不会写入
        super.log(level, args, meta);
    }

    /**
     * 自定义音讯花样
     * 能够依据自身的营业需求自行定义
     * @param { String } level
     */
    messageFormat({
        level
    }) {
        const { ctx } = this;
        const params = JSON.stringify(Object.assign({}, ctx.request.query, ctx.body));

        return [
            moment().format('YYYY/MM/DD HH:mm:ss'),
            ctx.request.get('traceId'),
            utils.serviceIPAddress,
            utils.clientIPAddress(ctx.req),
            level,
        ].join(utils.loggerDelimiter) + utils.loggerDelimiter;
    }
}

module.exports = AppTransport;
  • 东西

egg-logger-custom/lib/utils.js

const interfaces = require('os').networkInterfaces();

module.exports = {

    /**
     * 日记分隔符
     */
    loggerDelimiter: '[]',

    /**
     * 猎取当前效劳器IP
     */
    serviceIPAddress: (() => {
        for (const devName in interfaces) {
            const iface = interfaces[devName];

            for (let i = 0; i < iface.length; i++) {
                const alias = iface[i];

                if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
                    return alias.address;
                }
            }
        }
    })(),

    /**
     * 猎取当前要求客户端IP
     * 不安全的写法
     */
    clientIPAddress: req => {
        const address = req.headers['x-forwarded-for'] || // 推断是不是有反向代办 IP
        req.connection.remoteAddress || // 推断 connection 的长途 IP
        req.socket.remoteAddress || // 推断后端的 socket 的 IP
        req.connection.socket.remoteAddress;

        return address.replace(/::ffff:/ig, '');
    },

    clientIPAddress: ctx => {    
        return ctx.ip;
    },
}

注重:以上猎取当前要求客户端IP的体式格局,假如你须要对用户的 IP 做限流、防刷限定,请不要运用如上体式格局,拜见 科普文:怎样捏造和猎取用户实在 IP ?,在 Egg.js 里你也能够经由过程 ctx.ip 来猎取,参考 前置代办形式

  • 初始化 Logger
egg-logger-custom/app.js
const Logger = require('egg-logger').Logger;
const ConsoleTransport = require('egg-logger').ConsoleTransport;
const AppTransport = require('./app/logger');

module.exports = (ctx, options) => {
    const logger = new Logger();

    logger.set('file', new AppTransport({
        level: options.fileLoggerLevel || 'INFO',
        file: `/var/logs/${options.appName}/bizLog/${options.appName}.log`,
    }, ctx));

    logger.set('console', new ConsoleTransport({
        level: options.consoleLevel || 'INFO',
    }));

    return logger;
}

以上关于日记定制花样开辟已好了,假如你有现实营业须要能够依据自身团队的需求,封装为团队内部的一个 npm 中间件来运用。

项目扩大

自定义日记中间件封装好以后,在现实项目运用中我们还须要一步操纵,Egg 供应了 框架扩大 功用,包括五项:Application、Context、Request、Response、Helper,能够对这几项举行自定义扩大,关于日记由于每次日记纪录我们须要纪录当前要求照顾的 traceId 做一个链路追踪,须要用到 Context(是 Koa 的要求上下文) 扩大项。

新建 app/extend/context.js 文件

const AppLogger = require('egg-logger-custom'); // 上面定义的中间件

module.exports = {
    get logger() { // 名字自定义 也能够是 customLogger
        return AppLogger(this, {
            appName: 'test', // 项目名称
            consoleLevel: 'DEBUG', // 终端日记级别
            fileLoggerLevel: 'DEBUG', // 文件日记级别
        });
    }
}

发起:关于日记级别,能够采纳设置中间如 Consul 举行设置,上线时日记级别设置为 INFO,当须要临盆题目排查时,能够动态开启 DEBUG 形式。关于 Consul 能够关注我之前写的 效劳注册发明 Consul 系列

项目运用

毛病日记纪录,直接会将毛病日记完全客栈信息纪录下来,而且输出到 errorLog 中,为了保证非常可追踪,必需保证一切抛出的非常都是 Error 范例,由于只要 Error 范例才会带上客栈信息,定位到题目。

const Controller = require('egg').Controller;

class ExampleController extends Controller {
    async list() {
        const { ctx } = this;

        ctx.logger.error(new Error('顺序非常!'));

        ctx.logger.debug('测试');

        ctx.logger.info('测试');
    }
}

终究日记打印花样以下所示:

2019/05/30 01:50:21[]d373c38a-344b-4b36-b931-1e8981aef14f[]192.168.1.20[]221.69.245.153[]INFO[]测试

contextFormatter自定义日记花样

Egg-Logger 最新版本支撑经由过程 contextFormatter 函数自定义日记花样,拜见之前 PR:support contextFormatter #51

运用也很简朴,经由过程设置 contextFormatter 函数即可,以下是简朴的运用

config.logger = {
    contextFormatter: function(meta) {
        console.log(meta);
        return [
            meta.date,
            meta.message
        ].join('[]')
    },
    ...
};

一样的在你的营业里关于须要打印日记的处所,和之前一样

ctx.logger.info('这是一个测试数据');

输出效果以下所示:

2019-06-04 12:20:10,421[]这是一个测试数据

日记切割

框架供应了 egg-logrotator 中间件,默许切割为按天切割,别的体式格局可参考官网自行设置。

  • 框架默许日记途径

egg-logger 模块 lib/egg/config/config.default.js

config.logger = {
    dir: path.join(appInfo.root, 'logs', appInfo.name),
    ...
};
  • 自定义日记目次

很简朴根据我们的需求在项目设置文件从新定义 logger 的 dir 途径

config.logger = {
    dir: /var/logs/test/bizLog/
}

如许是不是就能够呢?根据我们上面自定义的日记文件名花样(${projectName}-yyyyMMdd.log),貌似是不可的,在日记支解过程当中默许的文件名花样为 .log.YYYY-MM-DD ,参考源码

https://github.com/eggjs/egg-logrotator/blob/master/app/lib/day_rotator.js

 _setFile(srcPath, files) {
    // don't rotate logPath in filesRotateBySize
    if (this.filesRotateBySize.indexOf(srcPath) > -1) {
      return;
    }

    // don't rotate logPath in filesRotateByHour
    if (this.filesRotateByHour.indexOf(srcPath) > -1) {
      return;
    }

    if (!files.has(srcPath)) {
      // allow 2 minutes deviation
      const targetPath = srcPath + moment()
        .subtract(23, 'hours')
        .subtract(58, 'minutes')
        .format('.YYYY-MM-DD'); // 日记花样定义
      debug('set file %s => %s', srcPath, targetPath);
      files.set(srcPath, { srcPath, targetPath });
    }
 }
  • 日记支解扩大

中间件 egg-logrotator 预留了扩大接口,关于自定义的日记文件名,能够用框架供应的 app.LogRotator 做一个定制。

app/schedule/custom.js

const moment = require('moment');

module.exports = app => {
    const rotator = getRotator(app);

    return {
        schedule: {
            type: 'worker', // only one worker run this task
            cron: '1 0 0 * * *', // run every day at 00:00
        },
        async task() {
            await rotator.rotate();
        }
    };
};

function getRotator(app) {
    class CustomRotator extends app.LogRotator {
        async getRotateFiles() {
            const files = new Map();
            const srcPath = `/var/logs/test/bizLog/test.log`;
            const targetPath = `/var/logs/test/bizLog/test-${moment().subtract(1, 'days').format('YYYY-MM-DD')}.log`;
            files.set(srcPath, { srcPath, targetPath });
            return files;
        }
    }

    return new CustomRotator({ app });
}

经由支解以后文件展现以下:

$ ls -lh /var/logs/test/bizLog/
total 188K
-rw-r--r-- 1 root root 135K Jun  1 11:00 test-2019-06-01.log
-rw-r--r-- 1 root root  912 Jun  2 09:44 test-2019-06-02.log
-rw-r--r-- 1 root root  40K Jun  3 11:49 test.log

扩大:基于以上日记花样,能够采纳 ELK 做日记汇集、剖析、检索。

作者:五月君
链接:https://www.imooc.com/article…
泉源:慕课网

浏览引荐

    原文作者:五月君
    原文地址: https://segmentfault.com/a/1190000019401465
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞