chanjs 2.8.0 → 2.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -9
- package/core/App.js +2 -0
- package/core/bootstrap/error-handler.js +4 -1
- package/doc/00-README.md +2 -2
- package/doc/05-/345/267/245/345/205/267/344/270/216/346/240/241/351/252/214.md +19 -18
- package/index.js +1 -1
- package/middleware/log.js +43 -48
- package/package.json +1 -1
- package/utils/logger.js +19 -83
package/README.md
CHANGED
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
### 基础设施
|
|
30
30
|
|
|
31
31
|
- **高性能日志**:Pino 结构化日志输出
|
|
32
|
-
-
|
|
32
|
+
- **全链路请求追踪**:自动注入唯一 `requestId`(`X-Request-Id` 响应头 + `req.id` + `req.log`)
|
|
33
|
+
- **访问日志按人归因**:一行纯文本,身份列 `ip + (uid|fp|sid)`,可追踪单个用户/访客的完整访问路径
|
|
33
34
|
- **国际化多语言**:i18next,内存高速语言查找
|
|
34
35
|
- **全局事件总线**:轻量封装 Node.js EventEmitter,解耦业务事件
|
|
35
36
|
- **定时任务调度**:node‑cron,自带异常捕获、停机安全回收
|
|
@@ -109,21 +110,59 @@ chan.run((port) => { // 启动 HTTP 服务
|
|
|
109
110
|
|
|
110
111
|
### 1. 日志系统(Pino)
|
|
111
112
|
|
|
112
|
-
|
|
113
|
+
pino 单实例 + pino-pretty(以 stream 方式跑主进程),**唯一开关 `LOG_LEVEL`**(默认 `info`),不读 `NODE_ENV`。输出格式固定:
|
|
113
114
|
|
|
114
115
|
```
|
|
115
|
-
|
|
116
|
+
2026-09-04 23:01:16.811 [Chan] HTTP服务启动,监听端口:3000
|
|
117
|
+
2026-09-04 23:01:16.811 ip=127.0.0.1 uid=2 GET /base/menu/list 200 6ms
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
时间是本地时区 `yyyy-MM-dd HH:mm:ss.SSS`,后面直接接正文——没有 `level/pid/hostname/module` 固定头,也没有 pino-pretty 默认的方括号和冒号。输出一律 stdout:本地开发直接看控制台,线上由 pm2 收集(框架不写日志文件、不接自定义管道)。
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
import { logger } from "chanjs";
|
|
116
124
|
|
|
117
|
-
//
|
|
125
|
+
// 全局日志实例(logger 是具名导出,默认导出是 Chan 主类)
|
|
118
126
|
logger.info("启动完成");
|
|
119
127
|
logger.error("查询失败", err); // 自动识别 Error 对象
|
|
120
128
|
|
|
121
|
-
//
|
|
122
|
-
const
|
|
123
|
-
|
|
129
|
+
// 携带上下文用 pino 原生 child(如请求链路 req.log)
|
|
130
|
+
const reqLog = logger.child({ userId: 1 });
|
|
131
|
+
reqLog.warn("慢查询");
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
访问日志由 **pino-http** 官方中间件接管(`middleware/log.js` 内 `app.use(pinoHttp({ logger: root }))`),自动记录 `req/res/status/duration`,并挂载 `req.log` 供业务打点:
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
import pinoHttp from "pino-http";
|
|
138
|
+
import { root } from "../utils/logger.js";
|
|
139
|
+
|
|
140
|
+
app.use(pinoHttp({ logger: root }));
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`middleware/log.js` 只额外补两件 pino-http 不管的事:① 匿名访客自动种 `_sid`(30 天 httpOnly cookie,按浏览器归人);② 用 `customSuccessMessage` 把访问行拼成 morgan 一行,身份列 = 来源IP + 用户标识(登录打 `uid`,未登录有设备指纹打 `fp`,再否则打 `sid`),用于按人追踪访问路径:
|
|
144
|
+
|
|
124
145
|
```
|
|
146
|
+
# 游客首访(自动种 _sid)
|
|
147
|
+
2026-09-04 22:59:36.768 ip=127.0.0.1 sid=2aad120e7b1c85b9 GET /news/index.html 200 23ms
|
|
148
|
+
# 同一浏览器后续请求 → 同一 sid,可串出完整访问序列
|
|
149
|
+
2026-09-04 22:59:36.868 ip=127.0.0.1 sid=2aad120e7b1c85b9 GET /news/hydraulic/index.html 200 23ms
|
|
150
|
+
# 未登录但有设备指纹 → fp
|
|
151
|
+
2026-09-04 22:59:36.903 ip=127.0.0.1 fp=fp_dev_hash_9x GET /news/hydraulic/article-58.html 200 23ms
|
|
152
|
+
# 登录用户 → uid 优先
|
|
153
|
+
2026-09-04 22:59:37.161 ip=127.0.0.1 uid=2 GET /base/menu/list 200 7ms
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
四列身份的取值优先级(`middleware/log.js` 内 `who()`):
|
|
157
|
+
|
|
158
|
+
| 列 | 来源 | 说明 |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| `ip` | `getIp(req)`(取 `X-Forwarded-For` / `X-Real-IP`) | 常驻,每行必有 |
|
|
161
|
+
| `uid` | `req.user.uid` | 登录用户(后台 auth、前台会员 `optionalAuth`),优先级最高 |
|
|
162
|
+
| `fp` | cookie `_f` | 设备指纹,未登录但有指纹时打 |
|
|
163
|
+
| `sid` | cookie `_sid` | 匿名访客,首访由服务端生成并种 30 天 httpOnly cookie |
|
|
125
164
|
|
|
126
|
-
|
|
165
|
+
每个请求注入唯一 `requestId`(响应头 `X-Request-Id` + `req.id` + `req.log`),Controller 内可直接携带链路日志:
|
|
127
166
|
|
|
128
167
|
```
|
|
129
168
|
// 在 Controller 中
|
|
@@ -253,7 +292,7 @@ HTTP 服务 → 定时任务 → 缓存存储 → 数据库连接 → 事件总
|
|
|
253
292
|
| `NODE_ENV` | 运行环境标识(dev/prd) | dev |
|
|
254
293
|
| `PORT` | HTTP 监听端口 | 3000 |
|
|
255
294
|
| `LOCALE` | 默认语言 | zh‑CN |
|
|
256
|
-
| `LOG_LEVEL` |
|
|
295
|
+
| `LOG_LEVEL` | 日志输出级别(trace/debug/info/warn/error/fatal) | info |
|
|
257
296
|
| `TRUSTED_PROXIES` | 信任反向代理网段 | loopback |
|
|
258
297
|
| `SHUTDOWN_TIMEOUT` | 优雅停机超时时间(ms) | 5000 |
|
|
259
298
|
| `REDIS_ENABLED` | 是否开启 Redis | false |
|
package/core/App.js
CHANGED
|
@@ -10,6 +10,7 @@ import { register as registerShutdown, shutdown as runShutdown } from "../utils/
|
|
|
10
10
|
import { registerCoreMiddleware } from "./bootstrap/middleware.js";
|
|
11
11
|
import { loadModuleRouter, loadCommonRouter } from "./bootstrap/router-loader.js";
|
|
12
12
|
import { registerErrorHandler } from "./bootstrap/error-handler.js";
|
|
13
|
+
import { log } from "../middleware/log.js";
|
|
13
14
|
import { runHooks } from "./bootstrap/hook-runner.js";
|
|
14
15
|
import { loadDotEnv } from "../config/index.js";
|
|
15
16
|
import { event } from "./EventBus.js";
|
|
@@ -71,6 +72,7 @@ export default class Chan {
|
|
|
71
72
|
logger.info("[Chan] 核心中间件注册完成");
|
|
72
73
|
|
|
73
74
|
this.#setupApp();
|
|
75
|
+
log(this.app); // 访问日志中间件:框架内置,业务侧无感(早于 #mountRouter 以正确计时整请求)
|
|
74
76
|
await this.#loadRoutes();
|
|
75
77
|
this.#mountRouter();
|
|
76
78
|
logger.info("[Chan] 路由加载挂载完成");
|
|
@@ -68,7 +68,10 @@ export function registerErrorHandler(chan) {
|
|
|
68
68
|
const userId = req.user?.id ?? "-";
|
|
69
69
|
|
|
70
70
|
// 日志分级:业务错误 4xx → warn,系统错误 5xx → error(唯一打印点,杜绝重复刷屏)
|
|
71
|
-
|
|
71
|
+
// ⚠️ pino 的日志方法依赖 this,不能解引用后裸调(logFn = logger.warn; logFn(...) 会在
|
|
72
|
+
// pino/lib/tools.js 抛 TypeError: Cannot read properties of undefined (reading 'Symbol(pino.msgPrefix)'),
|
|
73
|
+
// 把真实的业务错误整个掩盖成 500),必须用箭头函数包一层保住 this
|
|
74
|
+
const logFn = httpStatus < 500 ? (msg) => logger.warn(msg) : (msg) => logger.error(msg);
|
|
72
75
|
logFn(JSON.stringify({
|
|
73
76
|
type: err.name || "UnknownError",
|
|
74
77
|
message: describeError(err), // 递归解析 cause 根因,支持 AggregateError
|
package/doc/00-README.md
CHANGED
|
@@ -52,7 +52,7 @@ graph TD
|
|
|
52
52
|
Err["错误 AppError 族<br/>(core/errors.js)"]
|
|
53
53
|
Sec["安全 jwt / filterXSS /<br/>checkKeywords / rate-limit / sign"]
|
|
54
54
|
Sto["存储 store / cache"]
|
|
55
|
-
Log["日志 logger
|
|
55
|
+
Log["日志 logger"]
|
|
56
56
|
Val["校验 validate / validateAll"]
|
|
57
57
|
Path["路径 utils.paths / file"]
|
|
58
58
|
end
|
|
@@ -191,7 +191,7 @@ export class DemoController extends Controller {
|
|
|
191
191
|
| [02-响应与错误.md](./02-响应与错误.md) | `success` / `fail` / `AppError` 族 / `CODE` 错误码 | 写接口必看 |
|
|
192
192
|
| [03-安全模块.md](./03-安全模块.md) | JWT 登录态、XSS 过滤、关键词校验、限流、加解密 | 做登录/评论/表单必看 |
|
|
193
193
|
| [04-存储与缓存.md](./04-存储与缓存.md) | `store`(Redis/内存)、`cache`(内存缓存) | 做缓存/限流/黑名单必看 |
|
|
194
|
-
| [05-工具与校验.md](./05-工具与校验.md) | `logger
|
|
194
|
+
| [05-工具与校验.md](./05-工具与校验.md) | `logger`、`Paths`、`safePath`、`validate` / `validateAll` | 日常工具 |
|
|
195
195
|
| [06-应用生命周期.md](./06-应用生命周期.md) | `Chan` 类、`beforeStart`、`loader`、`registry` | 想理解启动流程/写启动钩子 |
|
|
196
196
|
| [07-事件系统EventBus.md](./07-事件系统EventBus.md) | `EventBus` / `event` 事件总线 | 做模块解耦 / 异步通知 |
|
|
197
197
|
| [08-定时任务Task.md](./08-定时任务Task.md) | `Task` 定时任务(cron) | 做定时清理 / 同步 / 报表 |
|
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
# 05 · 工具与校验:logger /
|
|
1
|
+
# 05 · 工具与校验:logger / Paths / 文件安全 / validate / utils 工具集
|
|
2
2
|
|
|
3
3
|
日常开发最常用的小工具。导出方式分两类:
|
|
4
|
-
- **根包直接导出**:`logger
|
|
4
|
+
- **根包直接导出**:`logger`、`Paths`、`validate`、`validateAll`。
|
|
5
5
|
- **`utils` 命名空间导出**:`import { getIp, request, tree, ... } from "chanjs/utils/index.js"`,
|
|
6
6
|
或集中用 `import { utils } from "chanjs"` 后 `utils.getIp(...)`。
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
## 一、日志 `logger`
|
|
10
|
+
## 一、日志 `logger`
|
|
11
11
|
|
|
12
|
-
文件:`utils/logger.js
|
|
12
|
+
文件:`utils/logger.js`。基于 pino 单实例(`logger` 即 `root`),**dev/prd 均走 pino-pretty 人读输出**,
|
|
13
|
+
`base:null` + `ignore` 去掉 `pid/hostname/module/level` 固定头,时间只留 `HH:mm:ss`。例:
|
|
14
|
+
`[14:09:33]: GET /cms/field/findByMid?mid=11 200 3ms`
|
|
13
15
|
|
|
14
16
|
### 1.1 默认实例 `logger`
|
|
15
17
|
|
|
16
18
|
```js
|
|
17
|
-
import logger from "chanjs"; //
|
|
19
|
+
import { logger } from "chanjs"; // logger 是具名导出(默认导出是 Chan 主类)
|
|
18
20
|
|
|
19
21
|
logger.info("服务启动");
|
|
20
22
|
logger.warn("缓存未命中");
|
|
@@ -28,25 +30,25 @@ logger.debug("调试信息", { userId: 1 });
|
|
|
28
30
|
| `debug` | 1 | 调试(dev 默认开启)|
|
|
29
31
|
| `info` | 2 | 常规信息(prd 默认开启)|
|
|
30
32
|
| `warn` | 3 | 警告 |
|
|
31
|
-
| `error` | 4 |
|
|
33
|
+
| `error` | 4 | 错误 |
|
|
32
34
|
| `fatal` | 5 | 致命 |
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
**级别控制**:唯一环境变量 `LOG_LEVEL`(默认 `info`),低于该级别的日志不输出——不读 `NODE_ENV`,级别只看这一个变量。
|
|
37
|
+
**输出目标**:pino + pino-pretty,统一 stdout。本地开发直接看控制台;线上由 pm2 捕获 stdout 落盘(框架不写日志文件、不接自定义管道)。格式固定为「时间戳 + 正文」,`pid/hostname/module/level` 固定头已忽略。
|
|
36
38
|
|
|
37
|
-
### 1.2
|
|
39
|
+
### 1.2 子日志 / 链路日志
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
需要区分来源或携带上下文时,用 pino 原生 `child`(不要再造 logger 实例):
|
|
40
42
|
|
|
41
43
|
```js
|
|
42
|
-
import {
|
|
44
|
+
import { logger } from "chanjs";
|
|
43
45
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
//
|
|
46
|
+
const reqLog = logger.child({ userId: 1 }); // 绑定字段,每行自动附带
|
|
47
|
+
reqLog.info("用户登录");
|
|
48
|
+
// [14:09:33]: 用户登录 {"userId":1}
|
|
47
49
|
```
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
请求链路:`middleware/log.js` 已为每个请求注入 `req.log`(`logger.child({ requestId })`)与 `req.id`,Controller 内直接用。
|
|
50
52
|
|
|
51
53
|
### 1.3 传 Error 自动解析
|
|
52
54
|
|
|
@@ -283,8 +285,7 @@ const html = `<ul>${pages(page, total, 10, "/news/", "")}</ul>`;
|
|
|
283
285
|
|
|
284
286
|
| 导出 | 类型 | 一句话用途 |
|
|
285
287
|
|---|---|---|
|
|
286
|
-
| `logger`
|
|
287
|
-
| `createLogger(name)` | 函数 | 业务自建带标签日志 |
|
|
288
|
+
| `logger` | 实例 | 框架级日志(具名导出,pino 单实例)|
|
|
288
289
|
| `Paths` | 对象 | 项目目录常量(root/app/config/public/modules/common/helper/extend)|
|
|
289
290
|
| `safePath` | 函数 | 防路径穿越(深导入 `utils/file.js`)|
|
|
290
291
|
| `validate` | 函数 | 单来源 zod 校验中间件 |
|
|
@@ -301,7 +302,7 @@ const html = `<ul>${pages(page, total, 10, "/news/", "")}</ul>`;
|
|
|
301
302
|
|
|
302
303
|
## 七、常见坑
|
|
303
304
|
|
|
304
|
-
1. **`logger`
|
|
305
|
+
1. **`logger` 是具名导出**:`import { logger } from "chanjs"`(花括号);`chanjs` 的默认导出是 `Chan` 主类,别混。
|
|
305
306
|
2. **忘了 `await store` 但 `cache` 同步**:`store` 异步要 `await`,`cache` 同步不要 `await`。
|
|
306
307
|
3. **校验后读 `req.body` 而非 `req.validated`**:Express 5 下 `req.query` 只读,框架已用 `req.validated` 统一承载,优先用它。
|
|
307
308
|
4. **zod 数字不 coerce**:表单传来的是字符串,`z.number()` 会失败,记得 `z.coerce.number()`。
|
package/index.js
CHANGED
|
@@ -69,7 +69,7 @@ export { createRateLimitMiddleware } from "./security/rate-limit.js";
|
|
|
69
69
|
export { filterXSS, checkKeywords } from "./security/index.js";
|
|
70
70
|
|
|
71
71
|
// ===================== 统一日志 =====================
|
|
72
|
-
export { default as logger
|
|
72
|
+
export { default as logger } from "./utils/logger.js";
|
|
73
73
|
|
|
74
74
|
// ===================== 校验中间件 =====================
|
|
75
75
|
export { validate, validateAll } from "./middleware/validate.js";
|
package/middleware/log.js
CHANGED
|
@@ -1,52 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
2
|
import pinoHttp from "pino-http";
|
|
3
|
-
import { getIp } from "../utils/ip.js";
|
|
4
3
|
import { root } from "../utils/logger.js";
|
|
4
|
+
import { getIp } from "../utils/ip.js";
|
|
5
|
+
|
|
6
|
+
// 静态资源:不记日志、不种 cookie
|
|
7
|
+
const STATIC = /\.(ico|png|jpe?g|gif|svg|css|js|mjs|woff2?|ttf|eot|map|webp|avif)([?#].*)?$/;
|
|
8
|
+
|
|
9
|
+
// 一行访问日志:ip=1.2.3.4 sid=xx GET /url 200 13ms
|
|
10
|
+
// 框架保持通用:只记框架自己能产生的身份(ip + sid),业务身份(uid 等)与框架无关
|
|
11
|
+
const line = (req, res, ms) =>
|
|
12
|
+
[
|
|
13
|
+
`ip=${getIp(req)}`,
|
|
14
|
+
req.sid && `sid=${req.sid}`,
|
|
15
|
+
req.method,
|
|
16
|
+
req.originalUrl || req.url,
|
|
17
|
+
res.statusCode,
|
|
18
|
+
`${ms ?? 0}ms`,
|
|
19
|
+
]
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.join(" ");
|
|
22
|
+
|
|
23
|
+
// 访问日志:pino-http 官方中间件接管(监听 finish、算耗时、挂 req.log)
|
|
24
|
+
// 额外只做 pino-http 不管的一件事:匿名访客 _sid(首访种 30 天 httpOnly cookie,之后按浏览器归人)
|
|
25
|
+
export function log(app) {
|
|
26
|
+
app.use((req, res, next) => {
|
|
27
|
+
req.skipLog = STATIC.test(req.originalUrl || req.url);
|
|
28
|
+
if (!req.skipLog) {
|
|
29
|
+
const hasSid = Boolean(req.cookies?._sid);
|
|
30
|
+
req.sid = hasSid ? req.cookies._sid : randomBytes(8).toString("hex");
|
|
31
|
+
// 仅首访无 _sid 时种 cookie(避免每个响应都带 Set-Cookie 头)
|
|
32
|
+
if (!hasSid) {
|
|
33
|
+
res.cookie("_sid", req.sid, { maxAge: 30 * 24 * 3600 * 1000, httpOnly: true, sameSite: "lax" });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
next();
|
|
37
|
+
});
|
|
5
38
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*/
|
|
15
|
-
export function log(app, config = {}) {
|
|
16
|
-
app.use(pinoHttp({
|
|
17
|
-
logger: root,
|
|
18
|
-
// 自动记录请求日志(静态资源跳过)
|
|
19
|
-
autoLogging: {
|
|
20
|
-
ignore: (req) => {
|
|
21
|
-
const url = req.url || "";
|
|
22
|
-
return /\.(ico|png|jpg|jpeg|gif|svg|css|js|woff|woff2)$/.test(url);
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
// requestId 生成:优先复用上游 X-Request-Id
|
|
26
|
-
genReqId: (req, res) => {
|
|
27
|
-
const id = req.headers["x-request-id"] || randomUUID();
|
|
28
|
-
res.setHeader("X-Request-Id", id);
|
|
29
|
-
return id;
|
|
30
|
-
},
|
|
31
|
-
// 日志级别按状态码区分
|
|
32
|
-
customLogLevel: (req, res, err) => {
|
|
33
|
-
if (err || res.statusCode >= 500) return "error";
|
|
34
|
-
if (res.statusCode >= 400) return "warn";
|
|
35
|
-
return "info";
|
|
36
|
-
},
|
|
37
|
-
customSuccessMessage: (req, res) => {
|
|
38
|
-
return `${req.method} ${req.originalUrl || req.url} ${res.statusCode}`;
|
|
39
|
-
},
|
|
40
|
-
customErrorMessage: (req, res, err) => {
|
|
41
|
-
return `${req.method} ${req.originalUrl || req.url} ${res.statusCode} ${err.message}`;
|
|
42
|
-
},
|
|
43
|
-
serializers: {
|
|
44
|
-
req: (req) => ({
|
|
45
|
-
method: req.method,
|
|
46
|
-
url: req.originalUrl || req.url,
|
|
47
|
-
ip: getIp(req),
|
|
48
|
-
}),
|
|
49
|
-
res: (res) => ({ statusCode: res.statusCode }),
|
|
50
|
-
},
|
|
51
|
-
}));
|
|
39
|
+
app.use(
|
|
40
|
+
pinoHttp({
|
|
41
|
+
logger: root,
|
|
42
|
+
autoLogging: { ignore: (req) => req.skipLog },
|
|
43
|
+
customSuccessMessage: line,
|
|
44
|
+
customErrorMessage: (req, res, err, ms) => `${line(req, res, ms)} ${err?.message || ""}`.trim(),
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
52
47
|
}
|
package/package.json
CHANGED
package/utils/logger.js
CHANGED
|
@@ -1,86 +1,22 @@
|
|
|
1
1
|
import pino from "pino";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* - prod:纯 JSON,pm2 捕获 stdout
|
|
11
|
-
*/
|
|
12
|
-
const baseOptions = {
|
|
13
|
-
level: LOG_LEVEL,
|
|
14
|
-
timestamp: pino.stdTimeFunctions.isoTime,
|
|
2
|
+
import pretty from "pino-pretty";
|
|
3
|
+
|
|
4
|
+
// 本地时间 yyyy-mm-dd HH:mm:ss.l(pino 默认给 UTC 的 ISO 串,带 T/Z 不好读,这里直接格式化好)
|
|
5
|
+
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
6
|
+
const timestamp = () => {
|
|
7
|
+
const d = new Date();
|
|
8
|
+
const t = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`;
|
|
9
|
+
return `,"time":"${t}"`;
|
|
15
10
|
};
|
|
16
11
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** pino 根实例 */
|
|
31
|
-
export const root = pino(baseOptions);
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 将多参数调用适配为 pino 格式
|
|
35
|
-
* 兼容现有用法:logger.info("msg")、logger.error("msg", err)、logger.warn("msg", obj)
|
|
36
|
-
*
|
|
37
|
-
* @param {string} level - 日志级别
|
|
38
|
-
* @param {import("pino").Logger} instance - pino 实例
|
|
39
|
-
* @param {...any} args - 业务传入的参数
|
|
40
|
-
*/
|
|
41
|
-
function emit(level, instance, ...args) {
|
|
42
|
-
const msgParts = [];
|
|
43
|
-
const mergeObj = {};
|
|
44
|
-
|
|
45
|
-
for (const arg of args) {
|
|
46
|
-
if (arg instanceof Error) {
|
|
47
|
-
// Error 对象转为 pino 的 err 字段(自动序列化 stack)
|
|
48
|
-
mergeObj.err = arg;
|
|
49
|
-
msgParts.push(arg.message);
|
|
50
|
-
} else if (arg !== null && typeof arg === "object") {
|
|
51
|
-
Object.assign(mergeObj, arg);
|
|
52
|
-
} else {
|
|
53
|
-
msgParts.push(String(arg));
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const msg = msgParts.join(" ");
|
|
58
|
-
const hasMerge = Object.keys(mergeObj).length > 0;
|
|
59
|
-
|
|
60
|
-
if (hasMerge) {
|
|
61
|
-
instance[level](mergeObj, msg);
|
|
62
|
-
} else {
|
|
63
|
-
instance[level](msg);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* 创建带固定标签的日志实例
|
|
69
|
-
* @param {string} [label="Chan"] - 模块标签
|
|
70
|
-
* @returns {{trace: Function, debug: Function, info: Function, warn: Function, error: Function, fatal: Function}}
|
|
71
|
-
*/
|
|
72
|
-
export function createLogger(label = "Chan") {
|
|
73
|
-
const instance = root.child({ module: label });
|
|
74
|
-
return {
|
|
75
|
-
trace: (...args) => emit("trace", instance, ...args),
|
|
76
|
-
debug: (...args) => emit("debug", instance, ...args),
|
|
77
|
-
info: (...args) => emit("info", instance, ...args),
|
|
78
|
-
warn: (...args) => emit("warn", instance, ...args),
|
|
79
|
-
error: (...args) => emit("error", instance, ...args),
|
|
80
|
-
fatal: (...args) => emit("fatal", instance, ...args),
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** 默认导出:全局 logger 实例(标签 "Chan") */
|
|
85
|
-
const logger = createLogger("Chan");
|
|
86
|
-
export default logger;
|
|
12
|
+
// 输出格式:时间 + 空格 + 正文(pino-pretty 默认模板是 [time]: msg,方括号和冒号去不掉,
|
|
13
|
+
// 故忽略 time 字段、改由 messageFormat 把时间拼在行首)
|
|
14
|
+
export const root = pino(
|
|
15
|
+
{ level: (process.env.LOG_LEVEL || "info").trim(), base: null, timestamp },
|
|
16
|
+
pretty({
|
|
17
|
+
colorize: false,
|
|
18
|
+
ignore: "pid,hostname,level,time,req,res,responseTime,reqId",
|
|
19
|
+
messageFormat: "{time} {msg}",
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
export default root;
|