├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── dellog.sh ├── doc └── odp-log.md ├── index.js ├── lib └── util.js ├── package.json ├── test-2 ├── index.html ├── main.js └── styles.css └── test ├── README.md └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | log 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | data/ 10 | 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # Dependency directory 25 | # Deployed apps should consider commenting this line out: 26 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "singleQuote": true, 6 | "semi": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid", 10 | "jsxBracketSameLine": false 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, fansekey 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node Log 统计方案 2 | --- 3 | ## 这是什么 4 | 5 | 这是yog框架的log统计模块,支持中间件或者单独使用等方式,兼容ODP日志格式与配置。关于ODP的日志方案调研可查看[此文档](./doc/odp-log.md). 6 | 7 | 统计日志类型包括: 8 | 9 | ### server日志 10 | - access_log: web访问日志,按小时分日志 11 | - error_log: web错误日志,按小时分日志 12 | 13 | 访问日志统计方式为请求返还才触发。 14 | 15 | ### 应用日志 16 | - 每个app有各自独立的日志,日志名为app的名称,例如demo.log和demo.log.wf。 17 | - 可配置每个app是否使用独立的子目录存放自身日志,例如demo/demo.log。 18 | - 可配置每个app是否按小时切分日志。 19 | - 可配置每个app的日志级别。 20 | - 对于不属于任何app的node.js程序,日志名为unknown.log。 21 | 22 | 23 | ## 快速开始 24 | 25 | ### 1 初始化配置 26 | ``` 27 | var YLogger = require('yog-log'); 28 | var path = require('path'); 29 | 30 | var conf = { 31 | app: 'yog', //app名称,产品线或项目名称等 32 | log_path: path.join(__dirname, 'log'), //日志存放地址 33 | intLevel: 16 //线上一般填4,参见配置项说明 34 | } 35 | 36 | app.use(YLogger(conf)); 37 | ``` 38 | 39 | 填写此配置之后yog-log就开始统计访问日志。 40 | 41 | ### 2 调用接口统计应用日志 42 | 43 | 使用`getLogger`方法获取到日志模块实例,然后调用接口统计日志。 44 | 45 | ```javascript 46 | var YLogger = require('yog-log'); 47 | var logger = YLogger.getLogger(); //默认通过domain获取,单独使用请传递config 48 | logger.log('warning','msg');//or logger.warning('msg'); 49 | ``` 50 | 51 | 52 | ## 日志初始化配置项 53 | 54 | 配置项均有默认值,理论上不需要配置也能工作。推荐设置配置有:`level`、`app`、`log_path` 三项。 55 | 56 | 配置项 | 默认值 | 说明 57 | --------------- | ----- | --------------- 58 | app | unknown | app名称,推荐填写 59 | format | 见下 | 默认应用日志格式 60 | format_wf | 见下 | 默认的应用日志warning及fatal日志格式 61 | intLevel | 16 | log日志级别,高于此级别的日志不会输出 62 | auto_rotate | 1 | 是否自动切分 63 | use_sub_dir | 1 | 日志是否在二级目录打印,目录名为 `APP_NAME` 64 | log_path | 插件安装地址/log | 日志存放目录,注意需要设置 65 | data_path | 插件安装地址/data | 格式数据存放的目录,可不用设置 66 | IS_OMP | 0 | 是否开启omp日志,如果不接入omp,建议置为2 67 | debug | 0 | 是否使用debug模式直接在控制台输出日志 68 | 69 | ``` 70 | 71 | 默认`format`: 72 | %L: %t [%f:%N] errno[%E] logId[%l] uri[%U] user[%u] refer[%{referer}i] cookie[%{cookie}i] %S %M 73 | 74 | 默认的`format_wf `: 75 | %L: %{%m-%d %H:%M:%S}t %{app}x * %{pid}x [logid=%l filename=%f lineno=%N errno=%{err_no}x %{encoded_str_array}x errmsg=%{u_err_msg}x] 76 | 77 | ``` 78 | 79 | ## 应用日志等级 80 | 81 | | 日志等级 | 数据编号 | 统计说明 | 82 | | ---- | ---- | ---- | 83 | | FATAL | 1 | 打印FATAL | 84 | | WARNING | 2 | 打印FATAL和WARNING | 85 | | NOTICE | 4 | 打印FATAL、WARNING、NOTICE(线上程序正常运行时的配置) | 86 | | TRACE | 8 | 打印FATAL、WARNING、NOTICE、TRACE(线上程序异常时使用该配置)| 87 | | DEBUG | 16 | 打印FATAL、WARNING、NOTICE、TRACE、DEBUG(测试环境配 | 88 | 89 | ## response.emit(name,obj,level) 90 | 91 | 在router层使用emit方式可以避免每个文件都引入logger和获取实例。参数说明: 92 | 93 | - name :日志事件名称,固定为'log' 94 | - obj: string或者object格式。如果是string,认为是错误消息。如果是object,请认为是详细信息。正确格式为{'stack':e,'msg':'msg','errno':'010'},分别代表`错误堆栈`、`错误消息`、`错误码`。错误消息如果不填将使用错误堆栈的消息。 95 | - level : 日志等级字符串,见上。不区分大小写,不写默认为notice 96 | 97 | 如下所示: 98 | 99 | ```javascript 100 | res.emit('log',{'stack':e,'errno':120,'msg' :'error happened!'},'warning'); 101 | ``` 102 | 103 | ## getLogger(config) 104 | 105 | 当框架接收请求时,yog-log会新建一个实例,并保存到domain中,确保单次请求流程中调用的getLogger获取到的是同一个实例。 106 | 107 | 如果单独使用log不经过请求, getLogger会新建一个实例,此时应当传递config配置参数。 108 | 109 | ## log(level,obj) 110 | 111 | 提供统一的log方法打印日志。参数说明同response.emit。另外针对各个应用日志等级提供了相对应的方法。 112 | 113 | 请确保使用快捷方法时名称准确,否则程序将报错。 114 | 115 | - fatal : logger.fatal(obj) 116 | - warning : logger.warning(obj) 117 | - notice : logger.notice(obj) 118 | - trace : logger.trace(obj) 119 | - debug : logger.debug(obj) 120 | 121 | `注意` : logger为通过getLogger获取到的日志模块实例 。 122 | 123 | **自定义错误消息** 124 | 125 | 如果想在日志中填写自定义的日志字段用于追查错误,请在obj中加入custom对象,然后按照键值对应放在custom中。如下所示: 126 | 127 | ``` 128 | //router层 129 | res.emit('log',{ 130 | 'stack':e, //错误堆栈 131 | 'errno':120, //错误码 132 | 'msg' :'error happened!', //错误消息 133 | 'custom':{'key1' :'value1','key2':'value2'} //自定义消息 134 | }); 135 | 136 | //其他地方 137 | logger.log('warning', { 138 | 'stack':e, //错误堆栈 139 | 'errno':120, //错误码 140 | 'msg' :'error happened!', //错误消息 141 | 'custom':{'key1' :'value1','key2':'value2'} //自定义消息 142 | }); 143 | 144 | ``` 145 | `注意`custom字段默认只会在`warning`和`fatal`日志中展现 146 | 147 | 生成的错误日志将会类似于下面的格式。其中可以看到custom字段已自动添加到日志中: 148 | 149 | ``` 150 | WARNING: 07-03 16:44:55 yd * - [logid=868855481 filename=D:\fis\test\models\doc.js lineno=25 errno=120 key1=value1 key2=value2 errmsg=error%20happened!] 151 | ``` 152 | 153 | ## Debug支持 154 | 155 | 处于debug模式下Log将在控制台输出错误日志,并根据错误日志类型显示不同的颜色,方便开发人员调试(debug模式下依旧会写日志到文件)。有两种方法开启debug模式: 156 | 157 | - **开发时** :yog的config.json的yogLogger `arguments`添加参数debug : 1 即开启debug模式 158 | - **线上** : 无论在线上还是线下都可以在url中添加query参数`_node_debug=1` 开启debug模式 159 | 160 | 161 | ## 日志格式配置 162 | 163 | yog-log兼容ODP支持灵活的日志格式配置,以满足不同系统对日志的格式要求。如接入OMP时warning日志格式配置: 164 | 165 | ``` 166 | %L: %{%m-%d %H:%M:%S}t %{app}x * %{pid}x [logid=%l filename=%f lineno=%N errno=%{err_no}x %{encoded_str_array}x errmsg=%{u_err_msg}x] 167 | ``` 168 | 169 | 除非特殊情况,不建议随意修改日志格式配置。 170 | 171 | 格式配置方法如下: 172 | 173 | 字段 | 描述 174 | ------- | ---------------- 175 | %% | 百分比字符串 176 | %h | name or address of remote-host 177 | %t | 时间戳,支持自定义格式如`%{%d/%b/%Y:%H:%M:%S %Z}t` 178 | %i | HTTP-header字段 179 | %a | 客户端IP 180 | %A | server address 181 | %C | 单个或全部cookie 182 | %D | 请求消耗时间/ms 183 | %f | 物理文件名称 184 | %H | 请求协议 185 | %m | 请求方法 186 | %p | 服务端端口 187 | %q | 请求query 188 | %U | 请求URL 189 | %v | HOSTNAME 190 | %V | HTTP_HOST 191 | %L | 当前日志等级 192 | %N | 错误发生行数 193 | %E | 错误码 194 | %l | LogID 195 | %M | 错误消息 196 | %x | 内置的自定义数据,有pid、cookie、encoded_str_array等 197 | 198 | 199 | ## 测试说明 200 | 201 | 单元测试说明详见[此文档](./test/README.md) 202 | -------------------------------------------------------------------------------- /dellog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #三天内保存小时,一周之前的删除,三天到一周的保存天 4 | 5 | logDir=/home/webspeed/test/access #日志文件夹地址 6 | start=`date +%Y%m%d -d '7 days ago'` #7天前 7 | end=`date +%Y%m%d -d '3 days ago'` #3天前 8 | 9 | date=$start 10 | 11 | tomo() 12 | { 13 | echo `date +%Y%m%d -d "$1 1 days"`; 14 | } 15 | end="`tomo $end`" 16 | 17 | #删除7天前的日志 18 | cd $logDir 19 | find . -type f -mtime +7 -name "access.*" -exec rm -f {} \; 20 | 21 | 22 | #3天到7天前的日志合成为一个文件删除小时数据 23 | 24 | while [ "$date" != "$end" ] 25 | do 26 | #合并小时文件,天级别文件名为access.$date 27 | find -name "access.log.$date*" | sort | xargs cat > "access.$date" 28 | find -name "access.log.$date*" -exec rm -f {} \; 29 | date="`tomo $date`" 30 | done -------------------------------------------------------------------------------- /doc/odp-log.md: -------------------------------------------------------------------------------- 1 | # Node Log方案调研 2 | --- 3 | 4 | 参考ODP日志类型,node方案中主要需要实现以下三类日志 5 | 6 | ## 日志类型 7 | 8 | ### server日志 9 | 1. access_log: web访问日志,按小时分日志 10 | 2. error_log: web错误日志,按小时分日志 11 | 3. xx.log: 上次的启动错误信息,成功则清空 12 | 13 | ### node日志 14 | 注: node运行日志在server中自主添加,此处不作说明 15 | 16 | ### 应用日志 17 | - 每个app有各自独立的日志,日志名为app的名称,例如demo.log和demo.log.wf。 18 | - 可配置每个app是否使用独立的子目录存放自身日志,例如demo/demo.log。 19 | - 可配置每个app是否按小时切分日志。 20 | - 可配置每个app的日志级别。 21 | - 对于不属于任何app的php程序,日志名为unknown-app.log。 22 | 23 | ## server日志说明 24 | 25 | server日志通过Node中间件实现,做一层简单的封装,暂时不支持格式配置,访问日志采用与现有方式统一的格式,错误日志格式待定。 26 | 27 | ### web访问日志 28 | 29 | ODP默认lighttpd访问日志格式为: 30 | 31 | ``` 32 | "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{Cookie}i\" \"%{User-Agent}i\" %D" 33 | ``` 34 | 35 | nginx日志格式为: 36 | ``` 37 | '$remote_addr - $remote_user [$time_local] "$request" ' 38 | '$status $body_bytes_sent "$http_referer" "$http_cookie" "$http_user_agent" ' '$request_time $logid $tracecode' 39 | ``` 40 | 41 | 两种服务器日志格式基本一致,按小时生成存放,日志示例如下: 42 | 43 | ``` 44 | 127.0.0.1 - - [10/Jun/2014:22:01:34 +0800] "GET /question/149487428.html?fr=ala&word=%E6%AC%A7%E7%90%B3%E6%A9%B1%E6%9F%9C&bd_ts=1876010&bd_framework=1&bd_vip=1 HTTP/1.0" 200 164258 "-" "-" "mozila firefox 1.0.7" 0.232 0093913545 00939135452783909642061022 45 | ``` 46 | 47 | 48 | 49 | 访问日志可采用express默认的log[中间件](https://github.com/expressjs/morgan)。支持格式配置,如下所示: 50 | 51 | ```javascript 52 | exports.format('default', ':remote-addr - - [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"'); 53 | 54 | ``` 55 | 56 | 57 | ### web错误日志 58 | 59 | 如下所示: 60 | 61 | ``` 62 | 2014/06/10 22:01:33 [notice] 15084#0: *67319 "^/+question/(\w+).*?$" matches "/question/93573827.html", client: 127.0.0.1, -, -, server: tc-iknow-fmon23.tc.baidu.com, request: "GET /question/93573827.html?mzl=qb_xg_6&word=%E5%AD%95%E5%A6%87%E5%AD%95%E5%90%8C%E4%BD%8E&ssid=0&uid=0&fr=solved&step=4 HTTP/1.0", -, -, host: "tc-iknow-fmon23.tc.baidu.com", -, -, -, -, 63 | ``` 64 | 65 | 对于错误日志,一些保证node服务零宕机的模块支持输出错误日志。如forever: 66 | 67 | ```bash 68 | forever start -e err.log app.js //-e制定错误日志输出 69 | ``` 70 | 71 | 72 | ## 应用日志说明 73 | 74 | 应用日志兼容现有ODP日志方案,初期实现其关键配置(全部日志配置及部分格式配置)。 75 | 76 | ### 配置项说明 77 | 78 | 配置项 | 默认值 | 说明 79 | --------------- | ----- | --------------- 80 | format | 见下 | 参照format string格式 81 | format_wf | 见下 | 参照format string格式 82 | level | 16 | log日志级别 83 | auto_rotate | 1 | 是否自动切分 84 | use_sub_dir | 1 | 日志是否在二级目录打印,目录名为 `${APP_NAME}` 85 | log_path | 无 | 日志存放目录 86 | data_path | 无 | 格式数据存放的目录 87 | is_omp | 0 | 是否开启omp日志,如果不接入omp,建议置为2 88 | 89 | ODP默认配置如下: 90 | 91 | ``` 92 | # 日志级别 93 | # 1:打印FATAL 94 | # 2:打印FATAL和WARNING 95 | # 4:打印FATAL、WARNING、NOTICE(线上程序正常运行时的配置) 96 | # 8:打印FATAL、WARNING、NOTICE、TRACE(线上程序异常时使用该配置) 97 | # 16:打印FATAL、WARNING、NOTICE、TRACE、DEBUG(测试环境配置) 98 | level: 16 99 | 100 | # 是否按小时自动分日志,设置为1时,日志被打在some-app.log.2011010101 101 | auto_rotate: 1 102 | 103 | # 日志文件路径是否增加一个基于app名称的子目录,例如:log/some-app/some-app.log 104 | # 该配置对于unknown-app同样生效 105 | use_sub_dir: 1 106 | 107 | format: format: %L: %t [%f:%N] errno[%E] logId[%l] uri[%U] refer[%{referer}i] cookie[%{cookie}i] %S %M 108 | 109 | # 提供绝对路径,日志存放的根目录,只有非odp环境下有效 110 | log_path: /home/user/odp/log/ 111 | # 提供绝对路径,日志格式数据存放的根目录,只有非odp环境下有效 112 | data_path: /home/user/odp/data/ 113 | # 是否开启Omp日志, 0默认值(两个日志文件都开启),1,只打印omp的new日志,2只打印老日志 114 | is_omp: 0 115 | ``` 116 | 117 | 118 | ### 日志格式配置项 119 | 120 | format string 格式字符串,取自 lighttpd 文档,第一个表格是ODP目前支持的配置。第二个表格,是 ODP Log 库扩展的功能 121 | 122 | Option | Description 123 | ------- | ---------------- 124 | %% | a percent sign 125 | %h | name or address of remote-host 126 | %t | timestamp of the end-time of the request //param, show current time, param specifies strftime format 127 | %i | HTTP-header field //param 128 | %a | remote address 129 | %A | local address 130 | %C | cookie field (not supported) //param 131 | %D | time used in ms 132 | %e | environment variable //param 133 | %f | physical filename 134 | %H | request protocol (HTTP/1.0, ...) 135 | %m | request method (GET, POST, ...) 136 | %p | server port 137 | %q | query string 138 | %T | time used in seconds //support param, s, ms, us, default to s 139 | %U | request URL 140 | %v | server-name 141 | %V | HTTP request host name 142 | 143 | 144 | ODP Log 库扩展的功能 145 | 146 | Option | Description 147 | ------- | -------------- 148 | %L | Log level 149 | %N | line number 150 | %E | err_no 151 | %l | log_id 152 | %u | user 153 | %S | strArray, support param, takes a key and removes the key from %S 154 | %M | error message 155 | %x | ODP extension, supports various param, like log_level, line_number etc. 156 | 157 | 目前支持的 %x 扩展参数: 158 | 159 | log_level, line, class, function, err_no, err_msg, log_id, app, function_param, argv, encoded_str_array in %x, prepend u_ to key to urlencode before its value 160 | 161 | ### 必须实现的配置(默认配置) 162 | 163 | Option | Description 164 | ------- | ---------------- 165 | %L | 日志级别 166 | %t | 时间戳 167 | %f | 物理文件 168 | %N | 日志行数 169 | %E | 错误码 170 | %l | LogID 171 | %U | 请求URL 172 | %S | strArray, support param, takes a key and removes the key from %S 173 | %M | error message 174 | %referer | 请求referer 175 | %cookie | cookie 176 | 177 | 示例日志: 178 | ``` 179 | WARNING: 14-01-17 16:24:22 [/home/iknow/odp/php/phplib/bd/db/ConnMgr.php:336] errno[10007] logId[1462671896] uri[/uhome/api/answertask?req=status&ids=1178216,1178395,1178464,1178533,1178217,1178382,1178431,1178197,1178454,1178219,1178401,1178182,1178472,1178223,1178385,1178215,1178387,1178249,1178192,1178194,1178586,1178193,1178187,1178337,1178183,1178565,1178527&t=1389947063538] refer[http://tc-iknow-fmon23.tc.baidu.com:8080/uhome/task/answer] cookie[BAIDUID=569C327905368FD551D5B2A75FAB0528:FG=1; bdshare_firstime=1383557648820; Hm_lvt_b8a6bc2d9b2c98aa6f831e2a2eaefa7c=1384419205,1384771816,1385445007,1386654952; MCITY=-%3A; noah_magic_user_name=wangbibo; USER_NOAH=wangbibo; EXPIRE_NOAH=1390276643; SIG_NOAH=a568c3fd66d1d6a41171a7387e73992f; BDRCVFR[feWj1Vr5u3D]=I67x6TjHwwYf0; BDUSS=2hlVXZ2dERVcU5GTDdNeS1XSEhPOVZUUDdMSzhwVnJIOTlSQVljUWljRmQ4fjFTQVFBQUFBJCQAAAAAAAAAAAEAAADeuR43d2FuZ2JpYm82NgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF1m1lJdZtZST; IK_USERVIEW=1; IK_CID_77=1; IK_CID_83=1; IK_CID_74=1; IK_CID_1031=2; IK_569C327905368FD551D5B2A75FAB0528=44; IK_CID_80=1; H_PS_PSSID=1462_4261_4759_4888_5047_5031; IM_old=0|hqj6ky88] db_cluster[nik/pgc] db_host[10.38.120.57] db_port[5400] default_db[pgc] Connect to Mysql failed 180 | ``` 181 | 182 | ### 接口说明 183 | 184 | 主要需要实现的接口如下 185 | 186 | API | Description 187 | ------- | ---------------- 188 | init | 根据提供参数进行初始化 189 | debug | 日志接口 190 | trace | 日志接口 191 | notice | 日志接口 192 | warning | 日志接口 193 | fatal | 日志接口 194 | addNotice | 添加需要监视查看的变量或者数组,暂不实现 195 | genLogID | 获取日志ID,暂不实现 196 | getClientIp | 获取客户端IP,暂不实现 197 | 198 | 日志接口参数说明 199 | 200 | 参数 | 说明 201 | ------- | ---------------- 202 | str | 必填,用户想要打印在 log 中的特定字符串 203 | errno | 错误码,默认值0 204 | arrArgs | 用户想要打印在 log 中的和上下文有关的数组,默认为null 205 | depth | 深度,不实现 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | domain = require('domain'), 4 | url = require('url'), 5 | crypto = require('crypto'); 6 | 7 | const callsites = require('callsites'); 8 | 9 | const {mkdirp} = require('mkdirp'); 10 | 11 | var util = require('./lib/util.js'), 12 | colors = require('colors'); 13 | 14 | var data_path = __dirname + '/'; //模板地址默认在模块里 15 | var log_path = __dirname + '/log'; 16 | var LOGGER_CACHE = {}; 17 | var LOGFILE_CACHE = {}; 18 | 19 | //日志等级 20 | var LEVELS = { 21 | 0: 'ACCESS', 22 | 3: 'ACCESS_ERROR', 23 | //应用日志等级 ODP格式 24 | 1: 'FATAL', 25 | 2: 'WARNING', 26 | 4: 'NOTICE', 27 | 8: 'TRACE', 28 | 16: 'DEBUG' 29 | }; 30 | 31 | var LEVELS_REVERSE = {}; 32 | 33 | for (var num in LEVELS) { 34 | LEVELS_REVERSE[LEVELS[num]] = num; 35 | } 36 | 37 | //debug模式下应用日志等级对应的颜色 38 | var COLORS = { 39 | 1: 'red', 40 | 2: 'yellow', 41 | 3: 'magenta', 42 | 4: 'grey', 43 | 8: 'cyan', 44 | 16: 'grey' 45 | }; 46 | 47 | var Logger = function (opts, req) { 48 | //模板文件地址,可以不填 49 | if (opts && opts['data_path']) { 50 | data_path = opts['data_path']; 51 | } 52 | 53 | //用户只需要填写log_path配置 54 | if (opts && opts['log_path']) { 55 | log_path = opts['log_path']; 56 | } 57 | 58 | this.req = req || { 59 | headers: {} 60 | }; 61 | 62 | this.opts = this.extend( 63 | { 64 | stdout_only: false, // 只输出 stdout,不写文件 65 | debug: 0, 66 | intLevel: 16, 67 | auto_rotate: 1, 68 | use_sub_dir: 1, 69 | IS_ODP: true, 70 | IS_OMP: 0, 71 | IS_JSON: false, 72 | log_path: log_path, 73 | access_log_path: log_path + '/access', 74 | access_error_log_path: log_path + '/access', 75 | data_path: data_path + 'data' 76 | }, 77 | opts 78 | ); 79 | 80 | //保存一次错误及请求的详情信息 81 | this.params = {}; 82 | 83 | //var format_wf = '%L: %{%m-%d %H:%M:%S}t %{app}x * %{pid}x [logid=%l filename=%f lineno=%N errno=%{err_no}x %{encoded_str_array}x errmsg=%{u_err_msg}x]'; 84 | 85 | //[10/Jun/2014:22:01:35 +0800] 86 | //应用日志格式,默认wf日志与default日志一样 87 | this.format = { 88 | ACCESS: 89 | this.opts['access'] || 90 | '%h - - [%{%d/%b/%Y:%H:%M:%S %Z}t] "%m %U %H/%{http_version}i" %{status}i %b %{Referer}i %{Cookie}i %{User-Agent}i %D', 91 | ACCESS_ERROR: 92 | '%h - - [%{%d/%b/%Y:%H:%M:%S %Z}t] "%m %U %H/%{http_version}i" %{status}i %b %{Referer}i %{Cookie}i %{User-Agent}i %D', 93 | WF: 94 | this.opts['format_wf'] || 95 | '%L: %t [%f:%N] errno[%E] logId[%l] uri[%U] user[%u] refer[%{referer}i] cookie[%{cookie}i] custom[%{encoded_str_array}x] %S %M', 96 | DEFAULT: 97 | this.opts['format'] || 98 | '%L: %t [%f:%N] errno[%E] logId[%l] uri[%U] user[%u] refer[%{referer}i] cookie[%{cookie}i] custom[%{encoded_str_array}x] %S %M', 99 | STD: '%L: %{%m-%d %H:%M:%S}t %{app}x * %{pid}x [logid=%l filename=%f lineno=%N errno=%{err_no}x %{encoded_str_array}x errmsg=%{u_err_msg}x]', 100 | STD_DETAIL: 101 | '%L: %{%m-%d %H:%M:%S}t %{app}x * %{pid}x [logid=%l filename=%f lineno=%N errno=%{err_no}x %{encoded_str_array}x errmsg=%{u_err_msg}x cookie=%{u_cookie}x]' 102 | }; 103 | }; 104 | 105 | Logger.prototype = { 106 | fatal: function () { 107 | return this.log.call(this, 'FATAL', arguments[0]); 108 | }, 109 | notice: function () { 110 | return this.log.call(this, 'NOTICE', arguments[0]); 111 | }, 112 | trace: function () { 113 | return this.log.call(this, 'TRACE', arguments[0]); 114 | }, 115 | warning: function () { 116 | return this.log.call(this, 'WARNING', arguments[0]); 117 | }, 118 | debug: function () { 119 | return this.log.call(this, 'DEBUG', arguments[0]); 120 | }, 121 | //level表示日志I等级,obj表示错误消息或者错误选项 122 | log: function (level, obj) { 123 | level = String(level).toUpperCase(); // WARNING格式 124 | var intLevel = this.getLogLevelInt(level); // 2格式 125 | var format = this.getLogFormat(level); 126 | if (intLevel < 0 || !format) { 127 | return false; 128 | } 129 | var option = {}; 130 | if (obj) { 131 | if (typeof obj === 'string') { 132 | option['msg'] = obj; 133 | } else if (typeof obj === 'object') { 134 | option = obj; 135 | } 136 | } 137 | //解析错误堆栈信息 138 | this.parseStackInfo(option); 139 | //解析自定义字段,存放在对应ODP的 encoded_str_array中 140 | this.params['encoded_str_array'] = ''; 141 | if (option['custom']) { 142 | this.parseCustomLog(option['custom']); 143 | } 144 | 145 | if (intLevel === 0 || intLevel === 3) { 146 | //访问日志 147 | this.writeLog(intLevel, option, format); 148 | } else { 149 | //IS_OMP等于0打印两种格式日志,等于1打印STD日志,等于2打印WF/Default日志 150 | if (this.opts['IS_OMP'] === 0 || this.opts['IS_OMP'] === 2) { 151 | option['filename_suffix'] = ''; 152 | option['escape_msg'] = false; //错误消息不转义 153 | this.writeLog(intLevel, option, format); 154 | } 155 | 156 | if (this.opts['IS_OMP'] === 0 || this.opts['IS_OMP'] === 1) { 157 | option['filename_suffix'] = '.new'; 158 | option['escape_msg'] = true; //错误消息转义 159 | this.writeLog(intLevel, option, this.format['STD']); 160 | } 161 | } 162 | }, 163 | 164 | extend: function (destination, source) { 165 | for (var property in source) { 166 | if (source.hasOwnProperty(property)) { 167 | destination[property] = source[property]; 168 | } 169 | } 170 | return destination; 171 | }, 172 | 173 | getLogFormat: function (level) { 174 | level = level.toUpperCase(); 175 | var formats = this.format; 176 | if (this.getLogLevelInt(level) < 0) { 177 | return false; 178 | } 179 | var format = formats['DEFAULT']; //默认格式, 180 | // ACCESS为访问格式 181 | if (level === 'ACCESS') { 182 | format = formats['ACCESS']; 183 | } else if (level === 'ACCESS_ERROR') { 184 | format = formats['ACCESS_ERROR']; //访问错误 404 301等单独存储 185 | } else { 186 | //warning和fatal格式不一样,且单独存储 187 | if (level === 'WARNING' || level === 'FATAL') { 188 | format = formats['WF']; 189 | } 190 | } 191 | 192 | return format; 193 | }, 194 | /** 195 | * 解析错误信息,获取函数名文件行数等 196 | * @param {[type]} option [description] 197 | * @return {[type]} [description] 198 | */ 199 | parseStackInfo: function (option) { 200 | this.params['errno'] = option['errno'] || 0; //错误号 201 | this.params['error_msg'] = option['msg'] || ''; //自定义错误信息,%M默认不转义 202 | this.params['TypeName'] = ''; 203 | this.params['FunctionName'] = ''; 204 | this.params['MethodName'] = ''; 205 | this.params['FileName'] = ''; 206 | this.params['LineNumber'] = ''; 207 | this.params['isNative'] = ''; 208 | // 如果没有传msg,但是传了stack,则用stack信息替代msg 209 | if (option['stack']) { 210 | try { 211 | if (!option['msg']) { 212 | this.params['error_msg'] = this.opts['debug'] 213 | ? option['stack'] 214 | : String(option['stack']).replace(/(\n)+|(\r\n)+/g, ' '); 215 | } 216 | } catch (e) { 217 | //this.log('notice','wrong error obj'); 218 | } 219 | } 220 | }, 221 | 222 | //解析自定义字段,'custom'字段 223 | parseCustomLog: function (obj) { 224 | if ('object' !== typeof obj) { 225 | return false; 226 | } 227 | var items = []; 228 | for (var key in obj) { 229 | if (obj.hasOwnProperty(key)) { 230 | items.push(escape(key) + '=' + escape(obj[key])); 231 | } 232 | } 233 | if (items.length > 0) { 234 | this.params['encoded_str_array'] = items.join(' '); 235 | } 236 | }, 237 | 238 | //初始化请求相关的参数 239 | parseReqParams: function (req, res) { 240 | if (!req || !req.headers || !res) { 241 | return false; 242 | } 243 | this.params['CLIENT_IP'] = 244 | req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.headers['x-real-ip']; 245 | this.params['REFERER'] = req.headers['referer']; 246 | this.params['COOKIE'] = req.headers['cookie']; 247 | this.params['USER_AGENT'] = req.headers['user-agent']; 248 | this.params['SERVER_ADDR'] = req.headers.host; 249 | this.params['SERVER_PROTOCOL'] = String(req.protocol).toUpperCase(); 250 | this.params['REQUEST_METHOD'] = req.method || ''; 251 | this.params['SERVER_PORT'] = req.app.settings ? req.app.settings.port : ''; 252 | this.params['QUERY_STRING'] = this.getQueryString(req.originalUrl); 253 | this.params['REQUEST_URI'] = req.originalUrl; 254 | this.params['REQUEST_PATHNAME'] = req._parsedUrl ? req._parsedUrl.pathname : '-'; 255 | this.params['REQUEST_QUERY'] = req._parsedUrl ? req._parsedUrl.query : '-'; 256 | this.params['HOSTNAME'] = req.hostname; 257 | this.params['HTTP_HOST'] = req.headers.host; 258 | this.params['HTTP_VERSION'] = req.httpVersionMajor + '.' + req.httpVersionMinor; 259 | this.params['STATUS'] = res.statusCode ? res.statusCode : null; 260 | this.params['CONTENT_LENGTH'] = (res.getHeaders() || {})['content-length'] || '-'; 261 | this.params['BYTES_SENT'] = res.socket ? res.socket.bytesRead : 0; 262 | this.params['HEADERS'] = req.headers; 263 | this.params['pid'] = process.pid; 264 | }, 265 | 266 | /** 267 | * 获取请求的querystring 268 | * 269 | * @param {string} url 请求 url 270 | * @return {string} querystring 271 | */ 272 | getQueryString(rawUrl) { 273 | try { 274 | var urlObj = url.parse(rawUrl); 275 | return urlObj.query || ''; 276 | } catch (e) { 277 | console.log(e); 278 | } 279 | return ''; 280 | }, 281 | 282 | /** 283 | * ODP环境下日志的前缀为AppName,非ODP环境需要配置指定前缀 284 | * @return {[string]} [description] 285 | */ 286 | getLogPrefix: function () { 287 | if (this.opts.autoAppName && this.req && this.req.CURRENT_APP) { 288 | return this.req.CURRENT_APP; 289 | } 290 | if (this.opts['IS_ODP'] === true) { 291 | return this.opts['app']; 292 | } 293 | return 'unknow'; 294 | }, 295 | 296 | //获取logID,如果没有生成唯一随机数 297 | getLogID: function (req, logIDName) { 298 | var logId = 0; 299 | 300 | if (this.params['LogId']) { 301 | return this.params['LogId']; 302 | } 303 | 304 | if (req) { 305 | if (req.headers[logIDName]) { 306 | logId = parseInt(req.headers[logIDName], 10); 307 | } else if (parseInt(req.query['logid'], 10) > 0) { 308 | logId = parseInt(req.query['logid'], 10); 309 | } else if (parseInt(this.getCookie('logid'), 10) > 0) { 310 | logId = parseInt(this.getCookie('logid'), 10); 311 | } 312 | } 313 | 314 | if (logId === 0) { 315 | var obj = util.gettimeofday(); 316 | logId = (obj['sec'] * 100000 + obj['usec'] / 10 + Math.floor(Math.random() * 100)) & 0x7fffffff; 317 | } 318 | return logId; 319 | }, 320 | 321 | //获取日志文件地址。注意访问日志与应用日志的差异 322 | getLogFile: function (intLevel) { 323 | var prefix = this.getLogPrefix() || 'yog'; 324 | var logFile = '', 325 | log_path = ''; 326 | switch (intLevel) { 327 | case '0': //访问日志前缀默认access 328 | logFile = this.opts['access_log_file'] || 'access'; //访问日志 329 | log_path = this.opts['access_log_path'] || this.opts['log_path']; 330 | break; 331 | case '3': 332 | logFile = this.opts['access_error_log_file'] || 'error'; //访问日志 333 | log_path = this.opts['access_error_log_path'] || this.opts['log_path']; 334 | break; 335 | default: 336 | //错误日志为app前缀 337 | //是否使用子目录,app区分 338 | log_path = this.opts['use_sub_dir'] ? this.opts['log_path'] + '/' + prefix : this.opts['log_path']; 339 | logFile = prefix; 340 | } 341 | 342 | return log_path + '/' + logFile + '.log'; 343 | }, 344 | 345 | /** 346 | * 写入信息到日志文件中,异步方式 347 | * @param {[type]} intLevel [日志等级,整数] 348 | * @param {[type]} options [写入选项] 349 | * @param {[type]} log_format [日志格式] 350 | * @return {[type]} [description] 351 | */ 352 | writeLog: function (intLevel, options, log_format) { 353 | //日志等级高于配置则不输出日志 354 | if ((intLevel > this.opts['intLevel'] || !LEVELS[intLevel]) && !this.opts['debug']) { 355 | return false; 356 | } 357 | 358 | this.params['current_level'] = LEVELS[intLevel]; 359 | 360 | //日志文件名称 361 | var logFile = this.getLogFile(intLevel), 362 | filename_suffix = options['filename_suffix'] || '', 363 | errno = options['errno'] || 0; 364 | 365 | if (this.getLogLevelInt('WARNING') === intLevel || this.getLogLevelInt('FATAL') === intLevel) { 366 | logFile += '.wf'; 367 | } 368 | //文件后缀 369 | logFile += filename_suffix; 370 | var logFileType = logFile; 371 | //是否按小时自动切分 372 | if (this.opts['auto_rotate']) { 373 | logFile += '.' + util.strftime(new Date(), '%Y%m%d%H'); 374 | } 375 | 376 | //STD日志需将错误日志转义 377 | if (this.params['error_msg'] && !this.opts['debug']) { 378 | this.params['error_msg'] = 379 | options['escape_msg'] === true ? escape(this.params['error_msg']) : unescape(this.params['error_msg']); 380 | } 381 | 382 | var format = log_format || this.format['DEFAULT']; 383 | var str = this.getLogString(format, options); 384 | if (!str) { 385 | return false; 386 | } 387 | 388 | // stdout_only 主要是给容器化部署用的,开启后也写入控制台,但没颜色 389 | if (this.opts['stdout_only']) { 390 | console.log(str); 391 | // debug 模式,console.log输出颜色标记的日志 392 | } else if (this.opts['debug'] && COLORS[intLevel] && process.env.NODE_ENV === 'dev') { 393 | var color = COLORS[intLevel]; 394 | var _str = unescape(str); 395 | console.log(_str[color]); 396 | } 397 | 398 | if (!LOGFILE_CACHE[logFileType]) { 399 | LOGFILE_CACHE[logFileType] = {}; 400 | } 401 | 402 | // 获取此类文件的FD缓存 403 | var fdCache = LOGFILE_CACHE[logFileType]; 404 | 405 | if (!fdCache[logFile] && !this.opts['stdout_only']) { 406 | // 关闭老的日志流 407 | for (var oldFile in fdCache) { 408 | if (fdCache.hasOwnProperty(oldFile)) { 409 | try { 410 | fdCache[oldFile].end(); 411 | } catch (e) {} 412 | delete fdCache[oldFile]; 413 | } 414 | } 415 | var pathname = path.dirname(logFile); 416 | if (!fs.existsSync(pathname)) { 417 | mkdirp.sync(pathname); 418 | } 419 | fdCache[logFile] = fs.createWriteStream(logFile, { 420 | flags: 'a' 421 | }); 422 | } 423 | if (!this.opts['stdout_only']) { 424 | fdCache[logFile].write(str); 425 | } 426 | }, 427 | 428 | //获取字符串标识对应的日志等级,没有返回-1 429 | getLogLevelInt: function (level) { 430 | return LEVELS_REVERSE[level] || -1; 431 | }, 432 | 433 | /** 434 | * 获取日志字符串,,执行模板函数读取日志数据 435 | * @return {[type]} [description] 436 | */ 437 | getLogString: function (format, options) { 438 | if (this.opts['IS_JSON']) { 439 | const caller = callsites()[3]; 440 | const callerFileName = caller.getFileName(); 441 | const callerFunctionName = caller.getFunctionName(); 442 | return ( 443 | JSON.stringify({ 444 | date: new Date().toJSON(), 445 | level: this.params['current_level'], 446 | msg: options['msg'], 447 | file: callerFileName, 448 | function: callerFunctionName, 449 | uri: this.params['REQUEST_URI'] || '' 450 | }) + '\n' 451 | ); 452 | } 453 | 454 | if (!format) { 455 | return false; 456 | } 457 | if (!LOGGER_CACHE[format]) { 458 | try { 459 | var jsStr = this.parseFormat(format); //获取各个format对应的js执行函数字符串 460 | LOGGER_CACHE[format] = requireFromString(jsStr); 461 | } catch (e) { 462 | console.error(e); 463 | //mail("生成日志模板js失败"); 464 | return false; 465 | } 466 | } 467 | return LOGGER_CACHE[format](this, util) + '\n'; 468 | }, 469 | 470 | //生产环境是否应当使用 471 | md5: function (data, len) { 472 | var md5sum = crypto.createHash('md5'), 473 | encoding = typeof data === 'string' ? 'utf8' : 'binary'; 474 | md5sum.update(data, encoding); 475 | len = len || 10; 476 | return md5sum.digest('hex').substring(0, len); 477 | }, 478 | 479 | //解析日志配置,生成相应的模板函数 480 | parseFormat: function (format) { 481 | var regex = /%(?:{([^}]*)})?(.)/g; 482 | var m; 483 | var action = []; 484 | while ((m = regex.exec(format)) != null) { 485 | if (m.index === regex.lastIndex) { 486 | regex.lastIndex++; 487 | } 488 | var code = m[2], 489 | param = m[1]; 490 | switch (code) { 491 | case 'h': 492 | action.push("logger.getParams('CLIENT_IP')"); 493 | break; 494 | case 't': 495 | var _act = "util.strftime(new Date(),'%y-%m-%d %H:%M:%S')"; 496 | if (param && param !== '') { 497 | _act = "util.strftime(new Date(), '" + String(param) + "')"; 498 | } 499 | action.push(_act); 500 | break; 501 | case 'i': 502 | var key = String(param).toUpperCase().replace(/-/g, '_'); 503 | action.push("logger.getParams('" + key + "')"); 504 | break; 505 | // 不转换参数格式, 方便获取自定义 header 参数 506 | case 'I': 507 | action.push("logger.getParams('" + param + "')"); 508 | break; 509 | case 'a': 510 | action.push("logger.getParams('CLIENT_IP')"); 511 | break; 512 | case 'A': 513 | action.push("logger.getParams('SERVER_ADDR')"); 514 | break; 515 | case 'b': 516 | action.push("logger.getParams('CONTENT_LENGTH')"); 517 | break; 518 | case 'C': 519 | if (param === '') { 520 | action.push("logger.getParams('HTTP_COOKIE')"); 521 | } else { 522 | action.push("logger.getCookie('" + param + "')"); 523 | } 524 | break; 525 | case 'D': 526 | //暂不确定计算准确 527 | action.push("logger.getParams('REQUEST_TIME')"); 528 | break; 529 | case 'e': 530 | //TODO 531 | action.push("''"); 532 | break; 533 | case 'f': 534 | action.push("logger.getParams('FileName')"); 535 | break; 536 | case 'H': 537 | action.push("logger.getParams('SERVER_PROTOCOL')"); 538 | break; 539 | case 'm': 540 | action.push("logger.getParams('REQUEST_METHOD')"); 541 | break; 542 | case 'p': 543 | action.push("logger.getParams('SERVER_PORT')"); 544 | break; 545 | case 'q': 546 | action.push("logger.getParams('QUERY_STRING')"); 547 | break; 548 | case 'T': 549 | //TODO 550 | action.push("''"); 551 | break; 552 | case 'U': 553 | action.push("logger.getParams('REQUEST_URI')"); 554 | break; 555 | case 'v': 556 | action.push("logger.getParams('HOSTNAME')"); 557 | break; 558 | case 'V': 559 | action.push("logger.getParams('HTTP_HOST')"); 560 | break; 561 | case 'L': 562 | action.push("logger.getParams('current_level')"); 563 | break; 564 | case 'N': 565 | action.push("logger.getParams('LineNumber')"); 566 | break; 567 | case 'E': 568 | action.push("logger.getParams('errno')"); 569 | break; 570 | case 'l': 571 | action.push("logger.getParams('LogId')"); 572 | break; 573 | case 'u': 574 | //TODO 用户ID 用户名 575 | action.push("logger.getParams('user')"); 576 | break; 577 | case 'S': 578 | //TODO 579 | action.push("''"); 580 | break; 581 | case 'M': 582 | action.push("logger.getParams('error_msg')"); 583 | break; 584 | case 'x': 585 | //TODO 586 | if (param.indexOf('u_') == 0) { 587 | param = param.substr(2); 588 | } 589 | switch (param) { 590 | case 'log_level': 591 | action.push("logger.getParams('current_level')"); 592 | break; 593 | case 'line': 594 | action.push("logger.getParams('LineNumber')"); 595 | break; 596 | case 'function': 597 | action.push("logger.getParams('FunctionName')"); 598 | break; 599 | case 'err_no': 600 | action.push("logger.getParams('errno')"); 601 | break; 602 | case 'err_msg': 603 | action.push("logger.getParams('error_msg')"); 604 | break; 605 | case 'log_id': 606 | action.push("logger.getParams('LogId')"); 607 | break; 608 | case 'app': 609 | action.push('logger.getLogPrefix()'); 610 | break; 611 | /*case 'function_param': 612 | $action[] = 'Bd_Log::flattenArgs(Bd_Log::$current_instance->current_function_param)'; 613 | break;*/ 614 | /*case 'argv': 615 | $action[] = '(isset($GLOBALS["argv"])? Bd_Log::flattenArgs($GLOBALS["argv"]) : \'\')'; 616 | break;*/ 617 | case 'pid': 618 | action.push("logger.getParams('pid')"); 619 | break; 620 | case 'encoded_str_array': 621 | action.push("logger.getParams('encoded_str_array')"); 622 | break; 623 | case 'cookie': 624 | action.push("logger.getParams('cookie')"); 625 | break; 626 | default: 627 | action.push("''"); 628 | } 629 | break; 630 | case '%': 631 | action.push("'%'"); 632 | break; 633 | default: 634 | action.push("''"); 635 | } 636 | } 637 | 638 | var strformat = util.preg_split(regex, format); 639 | var logCode = "'" + strformat[0] + "'"; 640 | for (var i = 1; i < strformat.length; i++) { 641 | logCode = logCode + ' + ' + action[i - 1] + " + '" + strformat[i] + "'"; 642 | } 643 | var cmt = '/* Used for app ' + this.opts['app'] + '\n'; 644 | cmt += ' * Original format string:' + format.replace(/\*\//g, '* /'); 645 | 646 | var str = cmt + '*/ \n module.exports=function(logger, util){\n return ' + logCode + '; \n}'; 647 | return str; 648 | }, 649 | 650 | getParams: function (name) { 651 | if (this.params.hasOwnProperty(name) && this.params[name] !== undefined && this.params[name] !== '') { 652 | return this.params[name]; 653 | } 654 | name = name.toLowerCase(); 655 | if (this.params.HEADERS && this.params.HEADERS.hasOwnProperty(name) && this.params.HEADERS[name]) { 656 | return this.params.HEADERS[name]; 657 | } 658 | return '-'; 659 | }, 660 | 661 | setParams: function (name, value) { 662 | this.params[name] = value; 663 | }, 664 | 665 | getCookie: function (name) { 666 | name = String(name).replace(/(^\s*)|(\s*$)/g, ''); 667 | var match = String(this.getParams('COOKIE')).match(new RegExp(name + '=([^;]+)')); 668 | if (match) { 669 | return match[1]; 670 | } 671 | return false; 672 | } 673 | }; 674 | 675 | function requireFromString(src, filename) { 676 | var Module = module.constructor; 677 | var m = new Module(); 678 | m._compile(src, filename || 'whatever'); 679 | return m.exports; 680 | } 681 | 682 | module.exports = function (config) { 683 | config = config || {}; 684 | 685 | return function (req, res, next) { 686 | var current; 687 | var logger; 688 | current = domain.create(); 689 | logger = new Logger(config, req); 690 | current.add(logger); 691 | current.logger = logger; // Add request object to custom property 692 | 693 | function logRequest() { 694 | res.removeListener('finish', logRequest); 695 | res.removeListener('close', logRequest); 696 | //以下参数需要在response finish的时候计算 697 | logger.params['STATUS'] = res.statusCode ? res.statusCode : null; 698 | logger.params['CONTENT_LENGTH'] = (res.getHeaders() || {})['content-length'] || '-'; 699 | if (req._startAt) { 700 | var diff = process.hrtime(req._startAt); 701 | var ms = diff[0] * 1e3 + diff[1] * 1e-6; 702 | logger.params['REQUEST_TIME'] = ms.toFixed(3); 703 | } 704 | //不区分访问错误日志 705 | logger.log('ACCESS'); 706 | } 707 | 708 | //只在请求过来的时候才设置LogId 709 | logger.params['LogId'] = logger.getLogID(req, config.LogIdName || 'x_bd_logid'); 710 | logger.parseReqParams(req, res); 711 | 712 | //response-time启动埋点 713 | req._startAt = process.hrtime(); 714 | 715 | res.once('finish', logRequest); 716 | res.once('close', logRequest); 717 | 718 | res.on('log', function (e, level) { 719 | var option = e || {}; 720 | level = level || 'notice'; 721 | // logger.parseReqParams(req, res); 722 | logger.log(level, option); 723 | }); 724 | 725 | //只要url设置了_node_debug参数,则开启debug模式,console.log输出日志 726 | if (req.query && req.query._node_debug) { 727 | logger.opts['debug'] = 1; 728 | } 729 | 730 | current.run(next); 731 | }; 732 | }; 733 | 734 | module.exports.Logger = Logger; 735 | 736 | module.exports.getLogger = function (config) { 737 | if (process.domain && process.domain.logger) { 738 | return process.domain.logger; 739 | } 740 | return new Logger(config); 741 | }; 742 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | var util = module.exports = function () {}; 2 | 3 | 4 | /** 5 | * Date#strftime(format) -> String 6 | * - format (String): Formats time according to the directives in the given format string. Any text not listed as a directive will be passed through to the output string. 7 | * 8 | * Ruby-style date formatting. Format matchers: 9 | * 10 | * %a - The abbreviated weekday name (``Sun'') 11 | * %A - The full weekday name (``Sunday'') 12 | * %b - The abbreviated month name (``Jan'') 13 | * %B - The full month name (``January'') 14 | * %c - The preferred local date and time representation 15 | * %d - Day of the month (01..31) 16 | * %e - Day of the month without leading zeroes (1..31) 17 | * %H - Hour of the day, 24-hour clock (00..23) 18 | * %I - Hour of the day, 12-hour clock (01..12) 19 | * %j - Day of the year (001..366) 20 | * %k - Hour of the day, 24-hour clock w/o leading zeroes (0..23) 21 | * %l - Hour of the day, 12-hour clock w/o leading zeroes (1..12) 22 | * %m - Month of the year (01..12) 23 | * %M - Minute of the hour (00..59) 24 | * %p - Meridian indicator (``AM'' or ``PM'') 25 | * %P - Meridian indicator (``am'' or ``pm'') 26 | * %S - Second of the minute (00..60) 27 | * %U - Week number of the current year, 28 | * starting with the first Sunday as the first 29 | * day of the first week (00..53) 30 | * %W - Week number of the current year, 31 | * starting with the first Monday as the first 32 | * day of the first week (00..53) 33 | * %w - Day of the week (Sunday is 0, 0..6) 34 | * %x - Preferred representation for the date alone, no time 35 | * %X - Preferred representation for the time alone, no date 36 | * %y - Year without a century (00..99) 37 | * %Y - Year with century 38 | * %Z - Time zone name 39 | * %z - Time zone expressed as a UTC offset (``-04:00'') 40 | * %% - Literal ``%'' character 41 | * 42 | * http://www.ruby-doc.org/core/classes/Time.html#M000298 43 | * 44 | **/ 45 | var strftime = require('fast-strftime'); 46 | 47 | util.strftime = function (date, format) { 48 | return strftime(format, date); 49 | }; 50 | 51 | //javascript版本的preg_split 52 | util.preg_split = function (pattern, subject, limit, flags) { 53 | // http://kevin.vanzonneveld.net 54 | // + original by: Marco Marchi?? 55 | // * example 1: preg_split(/[\s,]+/, 'hypertext language, programming'); 56 | // * returns 1: ['hypertext', 'language', 'programming'] 57 | // * example 2: preg_split('//', 'string', -1, 'PREG_SPLIT_NO_EMPTY'); 58 | // * returns 2: ['s', 't', 'r', 'i', 'n', 'g'] 59 | // * example 3: var str = 'hypertext language programming'; 60 | // * example 3: preg_split('/ /', str, -1, 'PREG_SPLIT_OFFSET_CAPTURE'); 61 | // * returns 3: [['hypertext', 0], ['language', 10], ['programming', 19]] 62 | // * example 4: preg_split('/( )/', '1 2 3 4 5 6 7 8', 4, 'PREG_SPLIT_DELIM_CAPTURE'); 63 | // * returns 4: ['1', ' ', '2', ' ', '3', ' ', '4 5 6 7 8'] 64 | // * example 5: preg_split('/( )/', '1 2 3 4 5 6 7 8', 4, (2 | 4)); 65 | // * returns 5: [['1', 0], [' ', 1], ['2', 2], [' ', 3], ['3', 4], [' ', 5], ['4 5 6 7 8', 6]] 66 | 67 | limit = limit || 0; 68 | flags = flags || ''; // Limit and flags are optional 69 | 70 | var result, ret = [], 71 | index = 0, 72 | i = 0, 73 | noEmpty = false, 74 | delim = false, 75 | offset = false, 76 | OPTS = {}, 77 | optTemp = 0, 78 | regexpBody = /^\/(.*)\/\w*$/.exec(pattern.toString())[1], 79 | regexpFlags = /^\/.*\/(\w*)$/.exec(pattern.toString())[1]; 80 | // Non-global regexp causes an infinite loop when executing the while, 81 | // so if it's not global, copy the regexp and add the "g" modifier. 82 | pattern = pattern.global && typeof pattern !== 'string' ? pattern : 83 | new RegExp(regexpBody, regexpFlags + (regexpFlags.indexOf('g') !== -1 ? '' : 'g')); 84 | 85 | OPTS = { 86 | 'PREG_SPLIT_NO_EMPTY': 1, 87 | 'PREG_SPLIT_DELIM_CAPTURE': 2, 88 | 'PREG_SPLIT_OFFSET_CAPTURE': 4 89 | }; 90 | if (typeof flags !== 'number') { // Allow for a single string or an array of string flags 91 | flags = [].concat(flags); 92 | for (i = 0; i < flags.length; i++) { 93 | // Resolve string input to bitwise e.g. 'PREG_SPLIT_OFFSET_CAPTURE' becomes 4 94 | if (OPTS[flags[i]]) { 95 | optTemp = optTemp | OPTS[flags[i]]; 96 | } 97 | } 98 | flags = optTemp; 99 | } 100 | noEmpty = flags & OPTS.PREG_SPLIT_NO_EMPTY; 101 | delim = flags & OPTS.PREG_SPLIT_DELIM_CAPTURE; 102 | offset = flags & OPTS.PREG_SPLIT_OFFSET_CAPTURE; 103 | 104 | var _filter = function (str, strindex) { 105 | // If the match is empty and the PREG_SPLIT_NO_EMPTY flag is set don't add it 106 | if (noEmpty && !str.length) { 107 | return; 108 | } 109 | // If the PREG_SPLIT_OFFSET_CAPTURE flag is set 110 | // transform the match into an array and add the index at position 1 111 | if (offset) { 112 | str = [str, strindex]; 113 | } 114 | ret.push(str); 115 | }; 116 | // Special case for empty regexp 117 | if (!regexpBody) { 118 | result = subject.split(''); 119 | for (i = 0; i < result.length; i++) { 120 | _filter(result[i], i); 121 | } 122 | return ret; 123 | } 124 | // Exec the pattern and get the result 125 | while (result = pattern.exec(subject)) { 126 | // Stop if the limit is 1 127 | if (limit === 1) { 128 | break; 129 | } 130 | // Take the correct portion of the string and filter the match 131 | _filter(subject.slice(index, result.index), index); 132 | index = result.index + result[0].length; 133 | // If the PREG_SPLIT_DELIM_CAPTURE flag is set, every capture match must be included in the results array 134 | if (delim) { 135 | // Convert the regexp result into a normal array 136 | var resarr = Array.prototype.slice.call(result); 137 | for (i = 1; i < resarr.length; i++) { 138 | if (result[i] !== undefined) { 139 | _filter(result[i], result.index + result[0].indexOf(result[i])); 140 | } 141 | } 142 | } 143 | limit--; 144 | } 145 | // Filter last match 146 | _filter(subject.slice(index, subject.length), index); 147 | return ret; 148 | } 149 | 150 | util.microtime = function (get_as_float) { 151 | // discuss at: http://phpjs.org/functions/microtime/ 152 | // original by: Paulo Freitas 153 | // example 1: timeStamp = microtime(true); 154 | // example 1: timeStamp > 1000000000 && timeStamp < 2000000000 155 | // returns 1: true 156 | 157 | var now = new Date() 158 | .getTime() / 1000; 159 | var s = parseInt(now, 10); 160 | 161 | return (get_as_float) ? now : (Math.round((now - s) * 1000) / 1000) + ' ' + s; 162 | } 163 | 164 | 165 | util.gettimeofday = function (return_float) { 166 | // discuss at: http://phpjs.org/functions/gettimeofday/ 167 | // original by: Brett Zamir (http://brett-zamir.me) 168 | // original by: Josh Fraser (http://onlineaspect.com/2007/06/08/auto-detect-a-time-zone-with-javascript/) 169 | // parts by: Breaking Par Consulting Inc (http://www.breakingpar.com/bkp/home.nsf/0/87256B280015193F87256CFB006C45F7) 170 | // revised by: Theriault 171 | // example 1: gettimeofday(); 172 | // returns 1: {sec: 12, usec: 153000, minuteswest: -480, dsttime: 0} 173 | // example 2: gettimeofday(true); 174 | // returns 2: 1238748978.49 175 | 176 | var t = new Date(), 177 | y = 0; 178 | 179 | if (return_float) { 180 | return t.getTime() / 1000; 181 | } 182 | 183 | // Store current year. 184 | y = t.getFullYear(); 185 | return { 186 | //sec: t.getUTCSeconds(), 187 | sec: parseInt(t / 1000), 188 | usec: t.getMilliseconds(), 189 | minuteswest: t.getTimezoneOffset(), 190 | // Compare Jan 1 minus Jan 1 UTC to Jul 1 minus Jul 1 UTC to see if DST is observed. 191 | dsttime: 0 + (((new Date(y, 0)) - Date.UTC(y, 0)) !== ((new Date(y, 6)) - Date.UTC(y, 6))) 192 | }; 193 | } 194 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yog-log", 3 | "version": "0.2.6", 4 | "description": "yog logger", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/fex-team/yog-log.git" 12 | }, 13 | "dependencies": { 14 | "callsites": "^3.1.0", 15 | "colors": "0.6.2", 16 | "fast-strftime": "1.1.1", 17 | "mkdirp": "2.1.5" 18 | }, 19 | "devDependencies": { 20 | "express": "4.17.1" 21 | }, 22 | "keywords": [ 23 | "fis", 24 | "yog", 25 | "express", 26 | "kraken-js" 27 | ], 28 | "author": "fansekey", 29 | "license": "BSD", 30 | "bugs": { 31 | "url": "https://github.com/fex-team/yog-log/issues" 32 | }, 33 | "homepage": "https://github.com/fex-team/yog-log" 34 | } 35 | -------------------------------------------------------------------------------- /test-2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | test 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test-2/main.js: -------------------------------------------------------------------------------- 1 | var element = document.querySelector("#greeting"); 2 | element.innerText = "Hello, world!"; 3 | -------------------------------------------------------------------------------- /test-2/styles.css: -------------------------------------------------------------------------------- 1 | #greeting { 2 | background-color: #F8F8F8; 3 | font-family: 'Open Sans', sans-serif; 4 | font-size: 24px; 5 | margin: 15px; 6 | } 7 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # yog-log 测试说明 2 | 3 | 此文档说明yog-log模块单测运行及测试点说明,目前已使用`mocha`对主要的功能点写单测,后续跟进功能升级需持续添加并更新此文档。 4 | 5 | ## 运行单测 6 | 7 | 单测使用mocha框架编写,文件为test目录`test.js` 8 | 9 | 1. 安装mocha `npm install -g mocha` 10 | 2. 在test目录运行`mocha test.js` 11 | 12 | 单测中需要判断程序异步执行结果且不支持callback的采取的是定时检测的方式,例如判断某个异步的log文件有没有生成。 13 | 14 | ## 测试点总结 15 | 16 | | 测试对象 | 测试点 | 是否已有单测 | 17 | | :---- | :---- | :---- | 18 | | 初始化配置 | 默认配置工作正常 | 有 | 19 | | 初始化配置 | 设置app名称且use_sub_dir=1时按建立app文件夹存储日志 | 有 | 20 | | 初始化配置 | 设置auto_rotate时按小时切分日志 | 有 | 21 | | 初始化配置 | 设置access_log_path把访问日志放在指定位置 | 有 | 22 | | 初始化配置 | 设置access_log_path异常时不报错(是否需要报错?) | 有 | 23 | | 生成日志 | 设置IS_OMP=0是有两种日志生成。.wf.xx及.wf.new.xx| 有 | 24 | | 生成日志 | 普通的应用日志生成正常| 有 | 25 | | 生成日志 | 访问日志生成正常| 有 | 26 | | 生成日志 | 10位LogID大于0且保证一小时内唯一 | 暂未支持 | 27 | | 生成日志 | 正常和错误的日志格式配置不报错 | 有 | 28 | | 日志格式 | 默认的应用日志格式生成结果正常 | 有 | 29 | | 日志格式 | 日期配置支持自定义 | 有 | 30 | | 日志格式 | 获取单个cookie的配置正常 | 有 | 31 | | 日志格式 | 获取单个cookie的解析正常 | 有 | 32 | | 日志格式 | {u_xx}x自定义项配置解析正常 | 有 | 33 | | 日志参数 | 支持自定义的日志参数打印 | 有 | 34 | | 日志参数 | LogID获取方式 | 实例测试 | 35 | | 日志调试 | 根据请求参数切换到调试模式 | 实例测试 | 36 | | 日志调试 | 根据日志等级使用不同颜色标记 | 实例测试 | 37 | | 日志调试 | 调试模式下去掉escape | 实例测试 | 38 | | 模块中间件 | 在express中间件中使用正常 | 实例测试 | 39 | | 模块中间件 | 通过domain传递logger实例正常 | 实例测试 | 40 | 41 | ## 实例使用教程 42 | 43 | 安装试用log模块,可本地或者全局安装yog-log。然后创建一个express实例,根据使用文档添加即可。 44 | 45 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 |  2 | /** 3 | * 测试点归纳 4 | * 1. 没有配置时默认配置运行正常 5 | * 2. 配置错误时运行正常 6 | * 3. 写日志及种类类型(兼容OMP等) 7 | * 4. 获取日志格式错误 8 | * 5. 解析request和response对象,判断是否正常 todo 9 | * 6. 获取LogID是否正常 10 | **/ 11 | var Logger = require("../index.js"); 12 | var assert = require("assert"); 13 | var util = require("../lib/util.js"); 14 | var fs = require("fs"); 15 | var path = require("path"); 16 | var http = require('http'); 17 | var app = require('express')(); 18 | 19 | //判断元素是否在数组中 20 | function in_array(array,e) { 21 | for(i=0;i= 0 ){ 102 | assert.fail(logFile , '/xx.log','should not use app subpath when use_sub_dir=0'); 103 | } 104 | 105 | //日志文件需包含app名称 106 | if(logFile.indexOf(app) < 0 ){ 107 | assert.fail(logFile , 'apptest.log','should have app name'); 108 | } 109 | }) 110 | }) 111 | 112 | 113 | //测试是否默认按小时切分设置 114 | describe('#auto_rotate', function(){ 115 | it('should ratote log in hour',function(done){ 116 | var logger = Logger.getLogger(); 117 | var logFile = logger.getLogFile() + ".wf." + util.strftime(new Date(),'%Y%m%d%H'); 118 | logger.log('warning', {'stack' : new Error("error happened!"),'errno' : 1234} ); 119 | var t=setInterval(function(){ 120 | if(fs.existsSync(logFile)){ 121 | clearInterval(t); 122 | done(); 123 | } 124 | },50); 125 | }) 126 | }) 127 | 128 | //测试访问日志地址配置 129 | describe('#access_log_path', function(){ 130 | it('should use access_log_path setting',function(){ 131 | var app = 'access',access_log_path = '/access/test2/'; 132 | var logger = Logger.getLogger({'app' : app,'access_log_path' : access_log_path }); 133 | var intLevel = logger.getLogLevelInt('ACCESS'); //访问日志等级编号 134 | var logFile = logger.getLogFile(intLevel) ; 135 | //如果日志路径中不包含自定义路径则不通过 136 | if(logFile.indexOf(access_log_path) < 0 ){ 137 | assert.fail(logFile , access_log_path + ".xxx",'should use custom access log path'); 138 | } 139 | 140 | }) 141 | }) 142 | 143 | 144 | //测试访问日志地址配置错误情况 145 | describe('#access_log_path', function(){ 146 | it('should work without error when path setting is wrong',function(){ 147 | var app = 'access',access_log_path = '/acdess^*%&%*(&())//test2/'; 148 | var logger = Logger.getLogger({'app' : app,'access_log_path' : access_log_path ,'is_omp':2}); 149 | logger.log("ACCESS"); 150 | var pathname = path.dirname(access_log_path); 151 | }) 152 | 153 | it('log_path',function(){ 154 | var app = 'access',log_path = '/test/test2/'; 155 | var logger = Logger.getLogger({'app' : app,'log_path' : log_path }); 156 | logger.log("ACCESS"); 157 | logger.log("ACCESS_ERROR"); 158 | logger.warning("test"); 159 | }) 160 | }) 161 | }) 162 | 163 | 164 | 165 | //测试日志类型及日志ID是否正常 166 | describe('Log', function(){ 167 | var logger = Logger.getLogger({'app':'log_test'}); 168 | this.timeout(5000); 169 | 170 | before(function(){ 171 | deleteFolder(logger.opts['log_path']); 172 | }) 173 | 174 | 175 | //测试兼容OMP的日志类型是否正常 176 | it('should have two type of log where IS_OMP=0', function(done){ 177 | var log_path = logger.opts['log_path'] + "/" + logger.opts['app']; 178 | logger.log('warning', {'stack' : new Error("error happened!"),'errno' : 1234} ); 179 | var t = setInterval(function(){ 180 | if(fs.existsSync(log_path)){ 181 | var logs = fs.readdirSync(log_path).sort(); 182 | if(logs.length > 0){ 183 | //排序后第一个是.wf.日志 最后一个是.wf.new日志 184 | if(logs[0].indexOf(".wf.") < 0 || logs[logs.length - 1].indexOf(".wf.new") < 0){ 185 | assert.fail(logs , ".wf.xx and .wf.new.xx",'should have two type of log when is_omp = 0'); 186 | } 187 | done(); 188 | clearInterval(t); 189 | } 190 | } 191 | },50); 192 | 193 | }) 194 | 195 | 196 | //测试默认的应用日志是否生成 197 | it('should have default app log', function(done){ 198 | logger.opts['app'] = "log_test2"; 199 | var log_path = logger.opts['log_path'] + "/" + logger.opts['app']; 200 | deleteFolder(log_path); 201 | logger.log('notice', {'stack' : new Error("error happened!"),'errno' : 1234} ); 202 | var t = setInterval(function(){ 203 | if(fs.existsSync(log_path)){ 204 | var logs = fs.readdirSync(log_path); 205 | if(logs.length > 0){ 206 | var stats = fs.statSync(log_path + "/" +logs[0]); 207 | var fileSize = stats["size"]; 208 | if(fileSize < 10){ 209 | throw new Error("no log record in file"); 210 | } 211 | done(); 212 | clearInterval(t); 213 | } 214 | } 215 | },50); 216 | 217 | }) 218 | 219 | //测试是否有访问日志生成 220 | it("should have access log ",function(done){ 221 | logger.log('ACCESS'); 222 | var log_path = logger.opts['log_path'] + "/access/"; 223 | deleteFolder(log_path); 224 | //存在访问日志文件且日志文件有记录 225 | var t = setInterval(function(){ 226 | if(fs.existsSync(log_path)){ 227 | var logs = fs.readdirSync(log_path); 228 | if(logs.length > 0){ 229 | var stats = fs.statSync(log_path + "/" +logs[0]); 230 | var fileSize = stats["size"]; 231 | if(fileSize < 10){ 232 | throw new Error("no log record in file"); 233 | } 234 | done(); 235 | clearInterval(t); 236 | } 237 | } 238 | },50); 239 | }) 240 | 241 | //测试LogID是否正常 242 | it('should generator unique LogID', function(){ 243 | 244 | //js暂时没找到毫秒级别内生成10位不重复随机数的办法 245 | /*var IDList = []; 246 | //一万个随机数判断是否唯一 247 | for (var i = 0 ; i <= 10000; i++) { 248 | var id = logger.getLogID(); 249 | if(in_array(IDList,id) ) { 250 | throw new Error("logID is not unique"); 251 | }else if( id < 0 ){ 252 | throw new Error("logID should > 0"); 253 | } 254 | IDList.push(id); 255 | }; 256 | */ 257 | }) 258 | 259 | //测试LogID是否正常 260 | it('LogId should equal to set',function(){ 261 | var req = { 262 | 'headers' : { 263 | 'x-forwarded-for' : '127.0.0.1', 264 | 'referer' : 'http://www.baidu.com', 265 | 'user-agent' : 'chrome', 266 | 'host' : 'host.baidu.com', 267 | 'HTTP_X_BD_LOGID' :'456' 268 | }, 269 | query :{ 270 | 'logid' : '789' 271 | }, 272 | 'app' : { 273 | 'setting' : { 274 | 'port' : 80 275 | } 276 | }, 277 | 'method' : 'GET', 278 | 'protocol' : 'http' 279 | }; 280 | var logger = Logger.getLogger(); 281 | logger.params['LogId']=123; 282 | assert.equal(123,logger.getLogID()); 283 | delete logger.params['LogId']; 284 | assert.equal(456,logger.getLogID(req)); 285 | delete req.headers.HTTP_X_BD_LOGID; 286 | assert.equal(789,logger.getLogID(req)); 287 | delete req.query.logid; 288 | logger.params['COOKIE'] = "name=app_test;logid=111;SECURE"; 289 | assert.equal(111,logger.getLogID(req)); 290 | delete logger.params['COOKIE']; 291 | var logid = logger.getLogID(); 292 | }) 293 | 294 | //默认的日志格式配置及错误的配置下不报错 295 | it("#getLogString should work without error",function(){ 296 | var format = logger.format; 297 | format['test1'] = '%L: %t [%f:%N] errno[%E] logId[%l] uri[%U] user[%u] refer[%{referer}i] cookie[%{cookie}i] %S %M'; 298 | format['test2'] = '%L: %t [%f:%N] errno[%E]logId[%l uri[%U] user[%u] refer[%{referer}i] cookie[%{cookie}i] %S %M'; 299 | format['test3'] = '%L: %t [%fN] errno[%E] logId[%l] uri[%U] user[%u] refer[%{referer}i] cookie[%{cookiei] %S M'; 300 | for(var f in format){ 301 | var str = logger.getLogString(format[f]); 302 | } 303 | }) 304 | 305 | }) 306 | 307 | 308 | //测试日志配置格式化主要参数是否正常 309 | describe('LogFomatter', function(){ 310 | 311 | 312 | 313 | var logger = Logger.getLogger(); 314 | 315 | //测试参数 316 | logger.params['CLIENT_IP'] = '127.0.0.1'; 317 | logger.params['REFERER'] = 'http://www.baidu.com'; 318 | logger.params['COOKIE'] = 'c1=cookie1;c2=cookie2'; 319 | logger.params['USER_AGENT'] = 'chrome' ; 320 | logger.params['SERVER_ADDR'] = '34.23.56.67' ; 321 | logger.params['SERVER_PROTOCOL'] = 'http'; 322 | logger.params['REQUEST_METHOD'] = 'get'; 323 | logger.params['SERVER_PORT'] = "80"; 324 | logger.params['QUERY_STRING'] = "?query=params"; 325 | logger.params['REQUEST_URI'] = "/test/test2"; 326 | logger.params['HOSTNAME'] = "hostname"; 327 | logger.params['HTTP_HOST'] = 'http_host'; 328 | logger.params['HTTP_VERSION'] = '1.1'; 329 | logger.params['STATUS'] = 200; 330 | logger.params['CONTENT_LENGTH'] = 500; 331 | logger.params['current_level'] = "NOTICE"; 332 | logger.params['errno'] = "123"; 333 | logger.params['error_msg'] = "error"; 334 | 335 | 336 | // 默认配置'%L: %t [%f:%N] errno[%E] logId[%l] uri[%U] user[%u] refer[%{referer}i] cookie[%{cookie}i] %S %M'; 337 | it('default app log format ',function(){ 338 | //默认配置去除时间 339 | var format = '%L: [%f:%N] errno[%E] logId[%l] uri[%U] user[%u] refer[%{referer}i] cookie[%{cookie}i] %S %M'; 340 | var str ="NOTICE: [-:-] errno[123] logId[-] uri[/test/test2] user[-] refer[http://www.baidu.com] cookie[c1=cookie1;c2=cookie2] error\n"; 341 | assert.equal(str,logger.getLogString(format)); 342 | }) 343 | 344 | //日期配置,支持灵活自定义 345 | it("#t time format",function(){ 346 | var format = '[%{%y/%m-%d %H:%M %Z}t]'; 347 | var util_time = util.strftime(new Date,'%y/%m-%d %H:%M %Z'); 348 | assert.equal("["+ util_time + "]\n",logger.getLogString(format)); 349 | }) 350 | 351 | //测试获取单个cookie的配置 352 | it("#C cookie format",function(){ 353 | var format = '[%{c1}C]'; 354 | assert.equal("[cookie1]\n",logger.getLogString(format)); 355 | }) 356 | 357 | //测试获取默认自定义项的配置 358 | it("#{u_xx}x custom item format",function(){ 359 | var format = '%{u_err_msg}x'; 360 | assert.equal("error\n",logger.getLogString(format)); 361 | }) 362 | 363 | 364 | //测试获取custom自定义字段的支持 365 | it("#custom log filed",function(){ 366 | var logger2 = Logger.getLogger(); 367 | var format = "%L: %{app}x * %{pid}x [logid=%l filename=%f lineno=%N errno=%{err_no}x %{encoded_str_array}x errmsg=%{u_err_msg}x]"; 368 | logger2.warning({'custom':{'key1':'value1','key2':'value2'},'errno' : '123','msg' : 'test_error'}); 369 | var str = "WARNING: unkown * - [logid=- filename=- lineno=- errno=123 key1=value1 key2=value2 errmsg=test_error]\n"; 370 | assert.equal(str,logger2.getLogString(format)); 371 | logger2.warning({'custom' : 'test','errno' : '456','msg' : 'test_error'}); 372 | str="WARNING: unkown * - [logid=- filename=- lineno=- errno=456 - errmsg=test_error]\n"; 373 | assert.equal(str,logger2.getLogString(format)); 374 | }) 375 | }) 376 | 377 | 378 | describe('method', function(){ 379 | var request = { 380 | 'headers' : { 381 | 'x-forwarded-for' : '127.0.0.1', 382 | 'referer' : 'http://www.baidu.com', 383 | 'user-agent' : 'chrome', 384 | 'host' : 'host.baidu.com', 385 | 'HTTP_X_BD_LOGID' : '456', 386 | 'cookie' : 'test=1;' 387 | }, 388 | query :{ 389 | 'logid' : '789' 390 | }, 391 | 'app' : { 392 | 'setting' : { 393 | 'port' : 80 394 | } 395 | }, 396 | 'method' : 'GET', 397 | 'protocol' : 'http' 398 | }; 399 | var response = { 400 | '_headers' : { 401 | 'content-length' : 20 402 | }, 403 | '_header' : {'test':1}, 404 | 'statusCode' : 200 405 | }; 406 | var logger = Logger.getLogger(); 407 | 408 | describe("#parseReqParams",function(){ 409 | logger.parseReqParams(request,response); 410 | it('content_length',function(){ 411 | assert.equal(20,logger.params['CONTENT_LENGTH']); 412 | }) 413 | it('status',function(){ 414 | assert.equal(200,logger.params['STATUS']); 415 | }) 416 | it('http_host',function(){ 417 | assert.equal('host.baidu.com',logger.params['HTTP_HOST']); 418 | }) 419 | 420 | respose = null; 421 | it('parseReqParams should return false',function(){ 422 | assert.equal(false,logger.parseReqParams(request,respose)); 423 | }) 424 | }) 425 | 426 | //测试LogID是否正常 427 | describe('#getLogID',function(){ 428 | it('params Logid',function(){ 429 | logger.params['LogId']=123; 430 | assert.equal(123,logger.getLogID()); 431 | }) 432 | 433 | it('http_x_bd_logid',function(){ 434 | delete logger.params['LogId']; 435 | assert.equal(456,logger.getLogID(request)); 436 | }) 437 | 438 | it('query.logid',function(){ 439 | delete request.headers.HTTP_X_BD_LOGID; 440 | assert.equal(789,logger.getLogID(request)); 441 | }) 442 | 443 | it('cookie logid',function(){ 444 | delete request.query.logid; 445 | logger.params['COOKIE'] = "name=app_test;logid=111;SECURE"; 446 | assert.equal(111,logger.getLogID(request)); 447 | }) 448 | 449 | it('random logid',function(){ 450 | delete logger.params['COOKIE']; 451 | var logid = logger.getLogID(); 452 | }) 453 | }) 454 | 455 | describe('#setParams',function(){ 456 | logger.setParams('name','wfg'); 457 | it('set params',function(){ 458 | assert.equal('wfg',logger.params['name']); 459 | }) 460 | }) 461 | 462 | describe('#getLogPrefix',function(){ 463 | logger.opts['IS_ODP']=false; 464 | it('should unkonw',function(){ 465 | assert.equal('unknow',logger.getLogPrefix()); 466 | }) 467 | }) 468 | 469 | describe('#writeLog',function(){ 470 | it('test writeLog',function(){ 471 | var options = {'custom':{'key1':'value1','key2':'value2'},'errno' : '123','msg' : 'shouldbereplace'}; 472 | var intLevel = logger.getLogLevelInt('abc'); 473 | var format; 474 | assert.equal(false,logger.writeLog(intLevel,options,format)); 475 | intLevel = logger.getLogLevelInt('WARNING'); 476 | delete logger.format['DEFAULT']; 477 | assert.equal(false,logger.writeLog(intLevel,options,format)); 478 | format = "%{u_err_msg}x"; 479 | var str = 'shouldbereplace\n'; 480 | logger.warning(options); 481 | logger.opts['debug']=1; 482 | logger.writeLog(intLevel,options,format); 483 | assert.equal(str,logger.getLogString(format)); 484 | }) 485 | }) 486 | 487 | describe('#parseFormat',function(){ 488 | it('test parseFormat',function(){ 489 | logger.params['CLIENT_IP']="127.0.0.1"; 490 | logger.params['SERVER_ADDR'] = 'server addr'; 491 | logger.params['HTTP_COOKIE'] = request.headers['cookie']; 492 | logger.params['SERVER_PORT'] = '8000'; 493 | logger.params['QUERY_STRING'] = 'debug=1'; 494 | logger.params['HOSTNAME'] = 'wfg'; 495 | logger.params['HTTP_HOST'] = 'http_host'; 496 | logger.params['LineNumber'] = 1; 497 | logger.params['FunctionName'] = 'myFunction'; 498 | logger.params['log_level'] = 'warning'; 499 | var format = "%{log_level}x %e %T: [logid=%{log_id}x client_ip=%a server_addr=%A %{}C server_port=%p query_string=%q hostname=%v http_host=%V LineNumber=%{line}x FunctionName=%{function}x]" 500 | var str = 'WARNING : [logid=- client_ip=127.0.0.1 server_addr=server addr test=1; server_port=8000 query_string=debug=1 hostname=wfg http_host=http_host LineNumber=1 FunctionName=myFunction]\n'; 501 | assert.equal(str,logger.getLogString(format)); 502 | }) 503 | }) 504 | }) 505 | 506 | //测试yog-log应用于express框架 507 | describe('module', function(){ 508 | it('test express',function(done){ 509 | var conf = {"level" : 16, //线上一般填4,参见配置项说明 510 | "app": "app_name", //app名称,产品线或项目名称等 511 | "log_path": __dirname+"/data/log",//日志存放地址' 512 | "data_path" :__dirname+ "/" 513 | }; 514 | app.use(Logger(conf)); 515 | app.use(function(req,res){ 516 | res.send('hello'); 517 | done(); 518 | }); 519 | app.listen(8827); 520 | var options = { 521 | hostname: '127.0.0.1', 522 | port: 8827, 523 | path: '/', 524 | method: 'POST' 525 | }; 526 | var req = http.request(options, function(res) { 527 | }); 528 | req.on('error', function(e) { 529 | }); 530 | req.write('data\n'); 531 | req.write('data\n'); 532 | req.end(); 533 | }) 534 | }) --------------------------------------------------------------------------------