Egg: 如何更优雅的使用egg的日志体系?

Created on 22 Jan 2018  ·  35Comments  ·  Source: eggjs/egg

以前用express+log4js,配置式参数使用的好好的。。
换做egg后,这个日志格式看得我各种别扭。


以下内容,我只能是尽可能去官方文档去获知解决方案,如果有已经提供的解决方案而我尚未看到,请原谅。


包括但不限于:

1、日志的时间精度:

应该是基于process.hrtime()的高精度时间,而不是new Date().getTime()吧?。
比如 egg-logger/lib/egg/context_logger.js 25行,这种计算方式得出的use时间,真的可用么?

2、请求响应日志

  • 我尚未在egg-日志中找到比较明确的说明,如有,麻烦告知下。
  • 需要记录的参数:请求方法,请求路由,请求参数(query,body;param收集的可能性不大?),作为一个api端需要的token或者其他authorization信息,响应时间,响应状态(http status)
  • 记录日志的格式自定义,是否有类似于log4jslayouts - Pattern Format方案?

尝试使用自写中间件完成accessLogger的功能呢,结果发现绑定到Context Logger时会因为

每行日志会自动记录上当前请求的一些基本信息, 如 [$userId/$ip/$traceId/${cost}ms $method $url]

参见Context Logger
查看源码,egg-logger/lib/egg/context_logger.js#43,并无可以配置的地方。

所以只能使用App Logger在加载中间件,将中间件绑定到app上,来实现access日志的记录

3、日志可封装和解析:

直接引入egg-logger后,原生的日志输出不符合基本需求,并且格式不统一

[2018-01-22 15:55:07.242] [cfork:master:34595] worker:34638 disconnect (exitedAfterDisconnect: true, state: disconnected, isDead: false, worker.disableRefork: false)
[2018-01-22 15:55:07.242] [cfork:master:34595] don't fork new work (refork: false)
2018-01-22 15:55:07,242 INFO 34595 [master] app_worker#4:34638 disconnect, suicide: true, state: disconnected, current workers: ["5"]
[2018-01-22 15:55:07.243] [cfork:master:34595] worker:34638 exit (code: 0, exitedAfterDisconnect: true, state: dead, isDead: true, isExpected: true, worker.disableRefork: false)

既然是企业级框架,就应该需要考虑每个企业有自己的一套日志体系吧?
按照log4js去配置是一件相对简单的事情,只不过expressresfinishclose监听需要耗费一部分代码去完成。
很多日志最后都是使用filebeat+logstash去采集的,日志格式统一,有利于L的快速过滤?

4、多级别日志分装

  • 在开发环境,可能需要debug级别的日志输出到一个单独的文件。而生产不需要此级别日志。
  • 应用部署中,并无给出解决方案?

5、全链路标记可配置

request - header中,因公司不同,可能使用的全链路唯一标志不同。
有的公司用traceId、而有的用request-id,诸如此类的,如果都需要去改源码去完成,是否对生产的部署是一种障碍?

这些原因大概是我选择关闭egg原生日志,#1667。

转而去寻找一种可能,使用koa-log4来复现已经成型的日志体系。


以上,如果我对日志的使用违反egg的日志规则,请指出

感谢egg

egg-logger discussion

Most helpful comment

赞。这里还是要澄清一点:

Egg 是微内核 + 插件生态的方式的。

这些高级功能,都是在插件里面去实现的,因此:

  • 官方不一定会,也不一定能去开发和维护所有的插件
  • 官方同学你可以视为是 2 个角色,一个是 Egg 微内核的维护者,一个是社区的插件开发者。
  • 作为前者这个角色时,我们关注的是微内核这块的特性,显然 tracer 这个不属于微内核的职责。
  • 作为后者这个角色时,你我并没有任何差别,我们都是基于自己的业务实践,分享出自己的实践产物- 插件。甚至由于业务场景方面的局限,我们作为这个角色时,在某个领域的实践,在我们的场景中是一个合适的方案,但对于外部场景未必一定是通用的最佳实践。

因此我们非常欢迎社区开发者能分享出自己的实践,来碾压我们的某些插件。

All 35 comments

思考的很细,感谢反馈。

  • 时间精度,在这个场合我个人认为是不需要太高精度的,一个请求耗时 5s 和 5.023s 区别不大,大部分时候我们都只会判断耗时在精确到秒的某个区间的占比。
  • 请求响应日志 目前我们更多是在前面的 Nginx 那层去记录,这块要自己定制一个并不难,ctx.logger 那块应该可以覆盖掉默认的 format 的。
  • 原生的日志输出 cfork 那个你可以理解为是一个第三方库输出到 stdout 的,这个肯定不好控制,但它并不会被写入到 file 里面的,不影响你分析。
  • 日志的格式是可以定制的,可以看下 egg-loggerlogger 文档
  • 多级别日志分装,每个 logger 都可以单独配置 level 的。
  • 全链路标记可配置 这块属于 tracelog 范畴,这块其实跟企业内部的架构有关,需要去定制化的。我们内部有鹰眼系统,以及对应的插件,有兴趣可以跟进和推动下这个 RFC

多谢回复

我的2、5实际上可以通过一套解决方案来实现,2的目的也是为了5.

即通过记录请求响应记录来复现全链路路由。

看了那个RFC,可能大家理解的方向不同。

抛个砖。。。


理论上,所谓tracer也好,全链路也罢。

#研发解决方案介绍#Tracing(鹰眼)中所说:

要能做到追踪每个请求的完整调用链路,收集调用链路上每个服务的性能数据,计算性能数据和比对性能指标(SLA),甚至在更远的未来能够再反馈到服务治理中,那么这就是分布式跟踪的目标了

个人理解

其目的,一言以蔽之:追踪从入到出的全链路路由,并且可以回溯各阶段响应情况。

主要几点:

  1. 链路路由:在同步、异步混合情况下,多服务/系统之间相互调用关系的记录
  2. 标记节点(物理机或者docker):明确标识产生日志的节点(可以用节点IP,前提是固定网卡且IP是固定分发的)
  3. 日志内容:明确标记节点,该节点的出、入时间戳,(因为可能有不确定的网络开销,所以每个节点单计入和出),该节点的业务标记(可以用链式记录,如A-B-D-E,就是一部分链路)
  4. 日志分析:(对日志分析系统,如ELK)更友善的记录形式(json),能快速从日志中分析出链路关系及各个节点的情况
  5. traceId:全链路的唯一标记,能唯一标记即可,直接用复杂点的取号器即可,最好能带上业务流标记,当你看到某条日志时,就知道这条日志归属到哪个业务流,尤其是一个节点应对多条业务流时(严格上,此种情况违反微服务架构,但考虑到某些基础服务可能会出现此种情况,尤其是小公司中没有基础架构组条件下)
  6. rpcId/appKey:我理解的就是一个全链路路由信息标记,能准确标记就好,每次叠加上一个即可,0.2.1看起来不如直接A-B-C直观,关键问题在于每个节点的key的分发要不重复,而且能通过链找到上一个节点(当然也可以只标记当前节点,不关心上一个节点,然后通过时间流回溯,这就需要各个节点的时间必须跟授时中心强同步)。

解决问题

前提

  • 结合时间流和业务流。
  • 忽略中间数据传输协议及传输方式,是直接通过http还是MQ队列、亦或是RPC。
  • 因为理论上业务流是明确的,数据是如何从A到Z的链路是预定义的,不然怎么写。
  • 所以每个节点记录时,是相信发送方是预定义的,直接采信其header中信息,然后追加本节点标记。

存储问题

  • 就使用最便捷,耗时短,非阻塞的文件存储即可。
  • 不要直接使用云日志,因为有网络开销,而且会给主进程造成负载(或竞争资源)。
  • 然后通过监听文件变化触发额外的(独立于业务主进程之外的)采集器(如filebeat)来向日志收集器(如logstash)推送日志

侵入式问题

  • 考虑到技术栈不同,越多的强制规范越会导致整个计划的流产。
  • 所以需要提供基于配置式的解决方案,然后提供多个/种中间件。
  • 当然,如果技术栈只考虑egg,那就简单了。

结合ELK体系的解决方案

简单说下思路

  • 每个节点

    • 配置该节点的业务标记 appKey
    • 从header中拉出 traceId(此key应可配置)
    • 从header中拉出 rpcId(此key应可配置), += '-' + appKey
    • 记录节点本地时间(应与授时中心时间强同步)
    • response响应时,合并request记录在本地项目中日志文件中生成一条日志

    [2018-01-24T11:25:16.747] [INFO] access - {"remoteIP":"127.0.0.1","originalUrl":"/","x-trace-id":"151382266795060c4-40d7-920d-09bb777a0f30","x-app-keys":"SC-CX-DD","req":{"method":"GET","header":{},"query":{},"body":{},"requestAt":"2018-01-24 11:25:16.256"},"res":{"status":200,"responseTime":"10.219ms","responseAt":"2018-01-24 11:25:16.266"}}
    
    

    json格式化下


{
    "remoteIP": "127.0.0.1",
    "originalUrl": "/",
    "x-trace-id":"151382266795060c4-40d7-920d-09bb777a0f30",
    "x-app-keys": "SC-CX-DD",
    "req": {
        "method": "GET",
        "header": {},
        "query": {},
        "body": {},
        "requestAt": "2018-01-24 11:25:16.256"
    },
    "res": {
        "status": 200,
        "responseTime": "10.219ms",
        "responseAt": "2018-01-24 11:25:16.266"
    }
}

  • ELK

    • 单节点单项目部署一个filebeat采集对应日志文件抛出给logstash

    • logstash过滤日志,通过grok解析日志信息,记录到ElasticSearch中

    • 基于Kibana二次开发一套页面展示信息


1和2,我在尝试写一个中间件来自己实现下。

赞。这里还是要澄清一点:

Egg 是微内核 + 插件生态的方式的。

这些高级功能,都是在插件里面去实现的,因此:

  • 官方不一定会,也不一定能去开发和维护所有的插件
  • 官方同学你可以视为是 2 个角色,一个是 Egg 微内核的维护者,一个是社区的插件开发者。
  • 作为前者这个角色时,我们关注的是微内核这块的特性,显然 tracer 这个不属于微内核的职责。
  • 作为后者这个角色时,你我并没有任何差别,我们都是基于自己的业务实践,分享出自己的实践产物- 插件。甚至由于业务场景方面的局限,我们作为这个角色时,在某个领域的实践,在我们的场景中是一个合适的方案,但对于外部场景未必一定是通用的最佳实践。

因此我们非常欢迎社区开发者能分享出自己的实践,来碾压我们的某些插件。

个人感觉:如果要接入elk的话,egg的日志格式就不太适用,希望可以自定义日志输出格式!

egg-logger 现在的自定义输出有什么问题么?不是一直支持么?

请求响应日志 目前我们更多是在前面的 Nginx 那层去记录,这块要自己定制一个并不难,ctx.logger 那块应该可以覆盖掉默认的 format 的。


ctx.loggerformat应该是这个吧,
https://github.com/eggjs/egg-logger/blob/master/lib/egg/context_logger.js#L54
这个formatter函数,并没有接受任何config的传入。
我尝试传入format函数,并无法改变此部分的逻辑。
因为这个地方的逻辑已经绑定了这个 context_logger的函数。。。

我自己封装了一个middleware,但并无法改变前边这一部分。

2018-01-27 15:40:32,534 INFO 48536 [-/::1/-/7ms GET /user/judge?a=2] {"remoteIP":"127.0.0.1","originalUrl":"/user/judge?a=2","req":{"method":"GET","header":{"Content-Type":"","token":""},"query":{"a":"2"},"body":{},"requestAt":"2018-01-27 15:40:32 "},"res":{"status":200,"responseTime":"3.830ms","responseAt":"2018-01-27 15:40:32 "}}

如果想要改变输出的format,应该是需要自定义Logger了,没法直接使用ctx.logger。

如果直接把
https://github.com/eggjs/egg-logger/blob/master/lib/egg/context_logger.js#L54
这个方法给改了,可以实现。但这样就侵入式修改egg的框架内容了。

贴一个我自己写了一部分的code

[!注意]以下代码并没有解决问题

/app/middleware/accessLogger.js中:

/**
 * Created by xxx on 2018/1/22.
 * Copyright© 2015-2020
 * @version 0.0.1 created
 */

'use strict';

/* eslint no-extend-native: ["error", { "exceptions": ["Date"] }] */
Date.prototype.format = DateForm; // Date原型链绑定时间格式化函数

/**
 * 请求响应日志
 * header信息获取 参见 https://eggjs.org/zh-cn/basics/controller.html#header
 * @return {accessLogger} 日志中间件
 */
module.exports = () => {
    return async function accessLogger(ctx, next) {
        const TIME_FORMAT = 'yyyy-MM-dd hh:mm:ss ';
        const { request, response } = ctx;
        const startedAt = process.hrtime(); // 获取高精度时间
        const log = { // 日志信息
            // uuid: ctx.get(), // 全链路唯一标记
            remoteIP: getIP(request), // 客户端IP
            originalUrl: request.originalUrl, // 请求地址
            // appKey: '', // 当前应用的标记
            req: {
                method: request.method,
                header: {
                    'Content-Type': ctx.get('Content-Type'),
                    token: ctx.get('auth_token'), // token权限
                },
                query: request.query,
                body: request.body,
                requestAt: new Date().format(TIME_FORMAT),
            },
            res: {},
        };
        await next();
        log.res = {
            status: response.status,
            responseTime: calcResponseTime(startedAt),
            responseAt: new Date().format(TIME_FORMAT),
        };
        const logger = ctx.getLogger('accessLogger');
        /**
         * 因Context Logger会增加 meta [$userId/$ip/$traceId/${cost}ms $method $url] 信息
         * 所以如果需要特定格式log,只能使用App Logger
         * 参见 https://eggjs.org/zh-cn/core/logger.html#context-logger
         * 参见 https://github.com/eggjs/egg-logger/blob/master/lib/egg/context_logger.js#L43
         */
        // console.info(Object.keys(logger._logger.options.formatter))
        logger._logger.options.formatter = meta => { // xxx 并没有用
            return '[' + meta.date + '] '
                + meta.level + ' '
                + meta.pid + ' '
                + meta.message;
        };
        logger.info(JSON.stringify(log));
    };
};


/**
 * nginx转发后获取实际IP信息
 * @param {object} req 请求参数
 * @return {string} 格式化IP
 */
function getIP(req) {
    let ip = req.get('x-forwarded-for'); // 获取代理前的ip地址
    if (ip && ip.split(',').length > 0) {
        ip = ip.split(',')[ 0 ];
    } else {
        ip = req.ip;
    }
    const ipArr = ip.match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g);
    return ipArr && ipArr.length > 0 ? ipArr[ 0 ] : '127.0.0.1';
}

/**
 * Date原型链绑定时间格式化函数
 * @param {string} format 格式化
 * @return {*} 格式化后时间
 */
function DateForm(format) {
    const o = {
        'M+': this.getMonth() + 1, // month
        'd+': this.getDate(), // day
        'h+': this.getHours(), // hour
        'm+': this.getMinutes(), // minute
        's+': this.getSeconds(), // second
        'w+': this.getDay(), // week
        'q+': Math.floor((this.getMonth() + 3) / 3), // quarter
        S: this.getMilliseconds(), // millisecond
    };
    if (/(y+)/.test(format)) { // year
        format = format.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length));
    }
    for (const k in o) {
        if (new RegExp('(' + k + ')').test(format)) {
            format = format.replace(RegExp.$1
                , RegExp.$1.length === 1 ? o[ k ] : ('00' + o[ k ]).substr(('' + o[ k ]).length));
        }
    }
    return format;
}

/**
 * 计算响应时间
 * @param {Array} startedAt 请求时间
 * @return {string} 响应时间字符串
 */
function calcResponseTime(startedAt) {
    const diff = process.hrtime(startedAt);
    // 秒和纳秒换算为毫秒,并保留3位小数
    return `${(diff[ 0 ] * 1e3 + diff[ 1 ] * 1e-6).toFixed(3)}ms`;
}

config/config.default.js

const path = require('path');

module.exports = appInfo => {
// 自定义日志
        customLogger: {
            // 请求响应日志
            accessLogger: {
                file: path.join(appInfo.root, 'logs/access.log'),
                format: meta => {
                    return '[' + meta.date + '] '
                        + meta.level + ' '
                        + meta.pid + ' '
                        + meta.message;
                },
                formatter: meta => {
                    return '[' + meta.date + '] '
                        + meta.level + ' '
                        + meta.pid + ' '
                        + meta.message;
                },
            },
        },
}

之后,format没起作用。

感觉应该是我什么地方的配置没加载。。。


很尴尬。。。😓

[!注意]以下代码会导致linux中文件句柄数打开过多,具体见命令lsof

修复方案见 https://github.com/eggjs/egg/issues/2006#issuecomment-395663629

——————【请不要使用以下代码】—————

一个可用的解决方案
全自写middleware来实现日志记录器

1. 在 app/middle/accessLogger.js

/**
 * Created by xxx on 2018/1/22.
 * Copyright© 2015-2020
 * @version 0.0.1 created
 */

'use strict';

const { Logger , FileTransport , ConsoleTransport } = require('egg-logger');

/**
 * 请求响应日志
 * header信息获取 参见 https://eggjs.org/zh-cn/basics/controller.html#header
 * 自定义日志器 参见https://github.com/eggjs/egg-logger#usage
 * @return {Function} 日志中间件
 */
module.exports = () => {
    return async function accessLogger(ctx, next) {
        // const TIME_FORMAT = 'yyyy-MM-dd hh:mm:ss S';
        const { request, response } = ctx;
        const startedAt = process.hrtime(); // 获取高精度时间
        const log = { // 日志信息
            // uuid: ctx.get(), // 全链路唯一标记
            remoteIP: getIP(request), // 客户端IP
            originalUrl: request.originalUrl, // 请求地址
            // appKey: '', // 当前应用的标记
            req: {
                method: request.method,
                header: {
                    'Content-Type': ctx.get('Content-Type'),
                    token: ctx.get('auth_token'), // token权限
                },
                query: request.query,
                body: request.body,
                requestAt: DateForm(),
            },
            res: {},
        };
        await next();
        log.res = {
            status: response.status,
            responseTime: calcResponseTime(startedAt),
            responseAt: DateForm(),
        };
        const logger = new Logger(); // 声明一个新的日志记录器
        // 配置文件输出/存储
        logger.set('file', new FileTransport({
            file: 'logs/access.log',
            level: 'INFO',
        }));
        // 配置控制台输出
        logger.set('console', new ConsoleTransport({
            level: 'DEBUG',
        }));
        // —————— TODO - 自定义日志输出格式 ————————
        logger.info('[' + DateForm() + '] [INFO] access - ' + JSON.stringify(log));
    };
};


/**
 * 获取实际IP信息
 * @param {object} req 请求参数
 * @return {string} 格式化IP
 */
function getIP(req) {
    let ip = req.get('x-forwarded-for'); // 获取代理前的ip地址
    if (ip && ip.split(',').length > 0) {
        ip = ip.split(',')[ 0 ];
    } else {
        ip = req.ip;
    }
    const ipArr = ip.match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g);
    return ipArr && ipArr.length > 0 ? ipArr[ 0 ] : '127.0.0.1';
}

/**
 * 时间格式化函数
 * @param {Date} date 时间
 * @param {string} format 格式化
 * @return {*} 格式化后时间
 */
function DateForm(date = new Date(), format = 'yyyy-MM-dd hh:mm:ss S') {
    const o = {
        'M+': date.getMonth() + 1, // month
        'd+': date.getDate(), // day
        'h+': date.getHours(), // hour
        'm+': date.getMinutes(), // minute
        's+': date.getSeconds(), // second
        'w+': date.getDay(), // week
        'q+': Math.floor((date.getMonth() + 3) / 3), // quarter
        S: date.getMilliseconds(), // millisecond
    };
    if (/(y+)/.test(format)) { // year
        format = format.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
    }
    for (const k in o) {
        if (new RegExp('(' + k + ')').test(format)) {
            format = format.replace(RegExp.$1
                , RegExp.$1.length === 1 ? o[ k ] : ('00' + o[ k ]).substr(('' + o[ k ]).length));
        }
    }
    return format;
}

/**
 * 计算响应时间
 * @param {Array} startedAt 请求时间
 * @return {string} 响应时间字符串
 */
function calcResponseTime(startedAt) {
    const diff = process.hrtime(startedAt);
    // 秒和纳秒换算为毫秒,并保留3位小数
    return `${(diff[ 0 ] * 1e3 + diff[ 1 ] * 1e-6).toFixed(3)}ms`;
}

2. 在config/config.default.js中或其他对应环境配置文件中

// 加载中间件
exports.middleware = [ 'accessLogger' ];

@atian25
就好比下面这条日志:
2018-01-27 15:40:32,534 INFO 48536 [-/::1/-/7ms GET /user/judge?a=2] {"remoteIP":"127.0.0.1","originalUrl":"/user/judge?a=2","req":{"method":"GET","header":{"Content-Type":"","token":""},"query":{"a":"2"},"body":{},"requestAt":"2018-01-27 15:40:32 "},"res":{"status":200,"responseTime":"3.830ms","responseAt":"2018-01-27 15:40:32 "}}2018-01-27 15:40:32,534 INFO 48536 [-/::1/-/7ms GET /user/judge?a=2] {"remoteIP":"127.0.0.1","originalUrl":"/user/judge?a=2","req":{"method":"GET","header":{"Content-Type":"","token":""},"query":{"a":"2"},"body":{},"requestAt":"2018-01-27 15:40:32 "},"res":{"status":200,"responseTime":"3.830ms","responseAt":"2018-01-27 15:40:32 "}}

前面这段 2018-01-27 15:40:32,534 INFO 48536 [-/::1/-/7ms GET /user/judge?a=2] 格式是定死的,你觉得没问题?日志系统都需要这样的格式吗?貌似并不是吧,一般开发者要么就是改变配置将输出格式设置为JSON,要么就是向上面说的自己写中间件,个人觉得不是很理想,还有就是日志的储存位置,就算我改了日志的摆放位置。系统的终端日志还是会打到根目录下(非本地开发 npm run dev),个人觉得没必要打出来,想知道打出来有什么意义?

@Webjiacheng

  1. 内置有支持 json 输出的:https://github.com/eggjs/egg-logger/blob/master/lib/egg/logger.js#L47
  2. 有自己的格式需求的时候,可以自定义 customLogger 来配置 format 的
  3. context logger 目前的 format 不支持覆盖,可以通过 2 的方式去定制自己的先,或者提 PR 优化下。

系统的终端日志还是会打到根目录下

这个没太看懂,默认的配置是 appInfo.root 下,参见文档 日志路径 ,你是可以修改配置的。

我就想知道错误日志,怎么能加上 ip ua 和请求参数信息...

要上下文必须用 ctx.logger

// app/context_logger.js
class ContextLogger extends require('egg-logger').EggContextLogger {
  paddingMessage() {
    return '';
  }
}

// app/extend/application.js
exports.ContextLogger =  require('../context_logger');

可以这样自定义

@popomore 如果我只想改掉某个 custom logger 的 context logger 的 formattor 呢?

这个不能改 format,只能在原来的 logger 上增加上下文信息

后面在 example 里面加个例子好了

我看着默认实现好像带了一些参数,但是实际打的日志,我没看到 ip(更新,是我看错了...)

get paddingMessage() {
  const ctx = this.ctx;

  // Auto record necessary request context infomation, e.g.: user id, request spend time
  // format: '[$userId/$ip/$traceId/$use_ms $method $url]'
  const userId = ctx.userId || '-';
  const traceId = ctx.tracer && ctx.tracer.traceId || '-';
  const use = ctx.starttime ? Date.now() - ctx.starttime : 0;
  return '[' +
    userId + '/' +
    ctx.ip + '/' +
    traceId + '/' +
    use + 'ms ' +
    ctx.method + ' ' +
    ctx.url +
  ']';
}

我的需求比较简单,只有错误日志才想多加几个字段

你把你的日志发一下

是否能定制一个通用日志格式,然后再根据需求,覆盖某种日志格式(比如错误日志)

格式里的字段,是否能传入日志对象里,还是说,必须按你说的方式去继承

@popomore 我的诉求是这样

  config.customLogger = {
    biz: {
      file: path.join(appInfo.root, 'logs/biz.log'),
      formatter(meta) {
        console.log(meta);
        return `### ${JSON.stringify(meta)}`;
      },
     // 或者再支持个 contextFormatter(meta, ctx)
    },
  };


    this.app.getLogger('biz').warn('app', 'msg');
    this.ctx.getLogger('biz').warn('ctx', 'msg');

实际输出, context logger 的 formatter 是无法自定义的

### {"level":"WARN","date":"2018-04-09 15:22:48,429","pid":53978,"hostname":"TZ-Mac.local","message":"app msg"}
2018-04-09 15:22:48,431 WARN 53978 [-/127.0.0.1/-/11ms GET /] ctx msg

egg 会打印访问日志么?还是说必须看 nginx 的访问日志?

@occultskyrong
[!注意]请求响应日志,自定义中间件

const { Logger , FileTransport , ConsoleTransport } = require('egg-logger');
const logger = new Logger(); // 声明一个新的日志记录器
// 配置文件输出/存储
logger.set('file', new FileTransport({
    file: 'logs/access.log',
    level: 'INFO',
}));
// 配置控制台输出
logger.set('console', new ConsoleTransport({
    level: 'DEBUG',
}));

您之前代码是每次写出都new一次。我放到开头的地方只new一次。会好很多吧。

@occultskyrong
兄弟 你这个代码 有坑。文件数量无限打开。

修复好的代码 不会出现无限打开文件 不释放

/**
 * Created by xxx on 2018/1/22.
 * Copyright© 2015-2020
 * @version 0.0.1 created
 */

'use strict';

const { Logger , FileTransport , ConsoleTransport } = require('egg-logger');
        const logger = new Logger(); // 声明一个新的日志记录器
        // 配置文件输出/存储
        logger.set('file', new FileTransport({
            file: 'logs/access.log',
            level: 'INFO',
        }));
        // 配置控制台输出
        logger.set('console', new ConsoleTransport({
            level: 'DEBUG',
        }));
/**
 * 请求响应日志
 * header信息获取 参见 https://eggjs.org/zh-cn/basics/controller.html#header
 * 自定义日志器 参见https://github.com/eggjs/egg-logger#usage
 * @return {Function} 日志中间件
 */
module.exports = () => {
    return async function accessLogger(ctx, next) {
        // const TIME_FORMAT = 'yyyy-MM-dd hh:mm:ss S';
        const { request, response } = ctx;
        const startedAt = process.hrtime(); // 获取高精度时间
        const log = { // 日志信息
            // uuid: ctx.get(), // 全链路唯一标记
            remoteIP: getIP(request), // 客户端IP
            originalUrl: request.originalUrl, // 请求地址
            // appKey: '', // 当前应用的标记
            req: {
                method: request.method,
                header: {
                    'Content-Type': ctx.get('Content-Type'),
                    token: ctx.get('auth_token'), // token权限
                },
                query: request.query,
                body: request.body,
                requestAt: DateForm(),
            },
            res: {},
        };
        await next();
        log.res = {
            status: response.status,
            responseTime: calcResponseTime(startedAt),
            responseAt: DateForm(),
        };

        // —————— TODO - 自定义日志输出格式 ————————
        logger.info('[' + DateForm() + '] [INFO] access - ' + JSON.stringify(log));
    };
};


/**
 * 获取实际IP信息
 * @param {object} req 请求参数
 * @return {string} 格式化IP
 */
function getIP(req) {
    let ip = req.get('x-forwarded-for'); // 获取代理前的ip地址
    if (ip && ip.split(',').length > 0) {
        ip = ip.split(',')[ 0 ];
    } else {
        ip = req.ip;
    }
    const ipArr = ip.match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g);
    return ipArr && ipArr.length > 0 ? ipArr[ 0 ] : '127.0.0.1';
}

/**
 * 时间格式化函数
 * @param {Date} date 时间
 * @param {string} format 格式化
 * @return {*} 格式化后时间
 */
function DateForm(date = new Date(), format = 'yyyy-MM-dd hh:mm:ss S') {
    const o = {
        'M+': date.getMonth() + 1, // month
        'd+': date.getDate(), // day
        'h+': date.getHours(), // hour
        'm+': date.getMinutes(), // minute
        's+': date.getSeconds(), // second
        'w+': date.getDay(), // week
        'q+': Math.floor((date.getMonth() + 3) / 3), // quarter
        S: date.getMilliseconds(), // millisecond
    };
    if (/(y+)/.test(format)) { // year
        format = format.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
    }
    for (const k in o) {
        if (new RegExp('(' + k + ')').test(format)) {
            format = format.replace(RegExp.$1
                , RegExp.$1.length === 1 ? o[ k ] : ('00' + o[ k ]).substr(('' + o[ k ]).length));
        }
    }
    return format;
}

/**
 * 计算响应时间
 * @param {Array} startedAt 请求时间
 * @return {string} 响应时间字符串
 */
function calcResponseTime(startedAt) {
    const diff = process.hrtime(startedAt);
    // 秒和纳秒换算为毫秒,并保留3位小数
    return `${(diff[ 0 ] * 1e3 + diff[ 1 ] * 1e-6).toFixed(3)}ms`;
}

@occultskyrong 你这个非常坑,根本不适用 楼上说的 文件描述符

@popomore 我的诉求是这样

  config.customLogger = {
    biz: {
      file: path.join(appInfo.root, 'logs/biz.log'),
      formatter(meta) {
        console.log(meta);
        return `### ${JSON.stringify(meta)}`;
      },
     // 或者再支持个 contextFormatter(meta, ctx)
    },
  };


    this.app.getLogger('biz').warn('app', 'msg');
    this.ctx.getLogger('biz').warn('ctx', 'msg');

实际输出, context logger 的 formatter 是无法自定义的

### {"level":"WARN","date":"2018-04-09 15:22:48,429","pid":53978,"hostname":"TZ-Mac.local","message":"app msg"}
2018-04-09 15:22:48,431 WARN 53978 [-/127.0.0.1/-/11ms GET /] ctx msg

自定义日志可以输出userId, traceId等上下文相关的内容吗? @atian25

用 ctx.getLogger

用 ctx.getLogger

这个不能自定义格式吧 @popomore

自定义 ContextLogger

直接实例化const EggLogger = require('egg-logger').EggLogger; 调自己的logger

这个日志真的是搞死我了。。。我觉得我是个十足的菜鸡,真的整不明白!!!!!!
首先致敬大佬,感谢创造轮子,但是这轮子我装不上啊, 啊,(;´༎ຶД༎ຶ`)

正题:
第一点:我发现logger.set的时候会出现logger上没有set的属性,应该是d.ts中的Logger没有继承Map造成的。
第二点:我想改变日期的格式,感谢上面大佬贡献的代码 通过中间件的事件已经实现,但是我发现我不会使用中间件。TOT,并且级别不能检索出来,需要再加一步级别的判定。
第三点:我还是感觉没有解决内置日志的格式更改,只能去customLog中配置一些自定义日志的格式。

我的诉求:自定义内置日志的格式

我可能说的很多地方都不对,我也是刚接触这方面,我搞了将近一周的时间了,真的不会了。。。

可能我还是在浮躁了,沉下心在研究一下吧,周一就要开例会报告了,啊,(〒︿〒)

内心十分期盼大佬的回答,谢谢~

@zheng199512

自定义 ContextLogger 可以看下这个示例: https://github.com/atian25/egg-showcase/pull/11

有没有直接使用 的 elk 日志中间件啊 @atian25

有没有直接使用 的 elk 日志中间件啊 @atian25

官方没维护,可以自己写个

看很多人这么煎熬。。。
感觉是可以用这个。。。来自定义中间件加进来

winston

满足绝大多数有关日志的需求。。。
日志分级、回收循环(rotate)、多管道(transport)

具体怎么用,自己看下文档。。。。上手已经是很简单了。。

至于与elk的结合。。。
自己用format来配置,然后Logstash里面配置对应的解析。。。

常规操作。。。(逃

看我上面 https://github.com/eggjs/egg/issues/2006#issuecomment-451334296 给的链接

看很多人这么煎熬。。。
感觉是可以用这个。。。来自定义中间件加进来

winston

满足绝大多数有关日志的需求。。。
日志分级、回收循环(rotate)、多管道(transport)

具体怎么用,自己看下文档。。。。上手已经是很简单了。。

至于与elk的结合。。。
自己用format来配置,然后Logstash里面配置对应的解析。。。

常规操作。。。(逃

在egg里面用winston,如何把traceId加进去?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

popomore picture popomore  ·  3Comments

lvgg3271 picture lvgg3271  ·  3Comments

dizhifeng picture dizhifeng  ·  3Comments

skyyangpeng picture skyyangpeng  ·  3Comments

aka99 picture aka99  ·  3Comments