Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

如何更优雅的使用egg的日志体系? #2006

Closed
occultskyrong opened this issue Jan 22, 2018 · 39 comments
Closed

如何更优雅的使用egg的日志体系? #2006

occultskyrong opened this issue Jan 22, 2018 · 39 comments

Comments

@occultskyrong
Copy link

occultskyrong commented Jan 22, 2018

以前用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

@atian25
Copy link
Member

atian25 commented Jan 23, 2018

思考的很细,感谢反馈。

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

@occultskyrong
Copy link
Author

occultskyrong commented Jan 24, 2018

多谢回复

我的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二次开发一套页面展示信息

@occultskyrong
Copy link
Author

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

@atian25
Copy link
Member

atian25 commented Jan 24, 2018

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

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

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

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

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

@Webjiacheng
Copy link

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

@atian25
Copy link
Member

atian25 commented Jan 25, 2018

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

@occultskyrong
Copy link
Author

occultskyrong commented Jan 27, 2018

请求响应日志 目前我们更多是在前面的 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的框架内容了。

@occultskyrong
Copy link
Author

occultskyrong commented Jan 27, 2018

贴一个我自己写了一部分的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没起作用。

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


很尴尬。。。😓

@occultskyrong
Copy link
Author

occultskyrong commented Jan 27, 2018

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

修复方案见 #2006 (comment)

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

一个可用的解决方案
全自写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' ];

@Webjiacheng
Copy link

@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),个人觉得没必要打出来,想知道打出来有什么意义?

@atian25
Copy link
Member

atian25 commented Jan 31, 2018

@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 下,参见文档 日志路径 ,你是可以修改配置的。

@musicode
Copy link

musicode commented Apr 9, 2018

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

@popomore
Copy link
Member

popomore commented Apr 9, 2018

要上下文必须用 ctx.logger

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

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

可以这样自定义

@atian25
Copy link
Member

atian25 commented Apr 9, 2018

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

@popomore
Copy link
Member

popomore commented Apr 9, 2018

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

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

@musicode
Copy link

musicode commented Apr 9, 2018

我看着默认实现好像带了一些参数,但是实际打的日志,我没看到 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
Copy link
Member

popomore commented Apr 9, 2018

你把你的日志发一下

@musicode
Copy link

musicode commented Apr 9, 2018

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

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

@atian25
Copy link
Member

atian25 commented Apr 9, 2018

@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

@musicode
Copy link

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

@wl496928838
Copy link

@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一次。会好很多吧。

@wl496928838
Copy link

wl496928838 commented Jun 8, 2018

@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`;
}

@yangchongduo
Copy link

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

@hupengfoot
Copy link

hupengfoot commented Nov 7, 2018

@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

@popomore
Copy link
Member

popomore commented Nov 7, 2018

用 ctx.getLogger

@hupengfoot
Copy link

用 ctx.getLogger

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

@popomore
Copy link
Member

popomore commented Nov 9, 2018

自定义 ContextLogger

@luckyxutao
Copy link

luckyxutao commented Nov 29, 2018

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

@zheng199512
Copy link

zheng199512 commented Dec 14, 2018

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

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

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

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

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

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

@atian25
Copy link
Member

atian25 commented Jan 4, 2019

@zheng199512

自定义 ContextLogger 可以看下这个示例: atian25/egg-showcase#11

@buzai
Copy link

buzai commented Jan 4, 2019

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

@atian25
Copy link
Member

atian25 commented Jan 4, 2019

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

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

@occultskyrong
Copy link
Author

occultskyrong commented Jan 4, 2019

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

winston

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

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

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

常规操作。。。(逃

@atian25
Copy link
Member

atian25 commented Jan 10, 2019

看我上面 #2006 (comment) 给的链接

@kenisad5566
Copy link

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

winston

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

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

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

常规操作。。。(逃

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

@TimLiu1
Copy link

TimLiu1 commented Jul 9, 2021

请求响应日志 目前我们更多是在前面的 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的框架内容了。

如下这样就可以了

config.logger = {
outputJSON: true,
formatter: (meta) => {
return meta.message
}
};

@mmrxia
Copy link

mmrxia commented Jul 13, 2021

第一步:在config.default.js中配置formatter

formatter(meta) {
    // 日期格式:yyyy-MM-dd HH-mm-ss SSS
    return `${moment(meta.date).format('YYYY-MM-DD HH:mm:ss SSS')}|${meta.message}`;
},

第二步:不要使用this.ctx.getLogger('xxxLogger'),使用this.ctx.app.getLogger('xxxLogger')

示例:this.ctx.app.getLogger('xxxLogger').info('hello world')

对比二者打印出的日志分别如下:

2021-07-12 17:58:59,023 INFO 90728 [-/127.0.0.1/-/17ms GET /] hello world
2021-07-12 17:59:44 620|hello world

@chenjianniu
Copy link

真的关键,Nice🤔,拿来吧你😁

@laoboxie
Copy link

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

winston

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

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

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

常规操作。。。(逃

请问你使用winston是不是只打业务日志?我理解egg的框架日志和egg-logger绑定在一起的,无法替换?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests