chanjs 2.7.17 → 2.8.1

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 CHANGED
@@ -29,7 +29,8 @@
29
29
  ### 基础设施
30
30
 
31
31
  - **高性能日志**:Pino 结构化日志输出
32
- - **全链路请求追踪**:pino‑http,自动注入唯一 `requestId`
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
- 开发环境彩色控制台输出;生产环境输出 JSON 结构化日志,适配 PM2 采集。API 向下完全兼容。
113
+ pino 单实例 + pino-pretty(以 stream 方式跑主进程),**唯一开关 `LOG_LEVEL`**(默认 `info`),不读 `NODE_ENV`。输出格式固定:
113
114
 
114
115
  ```
115
- import logger, { createLogger } from "chanjs";
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 dbLog = createLogger("DB");
123
- dbLog.warn("慢查询");
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
- 请求日志由 `pino‑http` 自动接管,每个请求分配唯一 `requestId`,Controller 内可直接携带链路日志:
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` | 日志输出级别 | dev:debug / prd:info |
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] 路由加载挂载完成");
package/core/loader.js CHANGED
@@ -60,17 +60,27 @@ export async function loadController(moduleName) {
60
60
  if (!inst || typeof inst !== "object") continue;
61
61
 
62
62
  // 原型方法批量bind,幂等标记避免重复绑定
63
- const proto = Object.getPrototypeOf(inst);
64
- if (!proto) continue;
65
- Object.getOwnPropertyNames(proto).forEach(key => {
66
- const fn = inst[key];
67
- if (key === "constructor" || typeof fn !== "function" || fn[BOUND_SYMBOL]) return;
68
- const boundFn = fn.bind(inst);
69
- Object.defineProperty(boundFn, BOUND_SYMBOL, {
70
- value: true, enumerable: false, writable: false, configurable: false
63
+ // 遍历整条原型链(含父类继承的方法):子类方法定义在自身原型、父类模板方法定义在父类原型,
64
+ // bind 直接原型会导致继承方法 this 丢失(Express 非点调用时 this=undefined)。
65
+ // ⚠️ 必须用 getOwnPropertyDescriptor 取 value:BaseComponent 的 app/config/db 是 getter-only,
66
+ // 其中 db 返回 knex 实例(typeof 为 function),直接 inst[key] 会触发 getter 且赋值只读属性抛错。
67
+ let proto = Object.getPrototypeOf(inst);
68
+ while (proto && proto !== Object.prototype) {
69
+ Object.getOwnPropertyNames(proto).forEach(key => {
70
+ if (key === "constructor") return;
71
+ const desc = Object.getOwnPropertyDescriptor(proto, key);
72
+ // 只 bind 数据方法(value 为函数);getter/setter 访问器跳过
73
+ if (!desc || typeof desc.value !== "function") return;
74
+ const fn = desc.value;
75
+ if (fn[BOUND_SYMBOL]) return;
76
+ const boundFn = fn.bind(inst);
77
+ Object.defineProperty(boundFn, BOUND_SYMBOL, {
78
+ value: true, enumerable: false, writable: false, configurable: false
79
+ });
80
+ inst[key] = boundFn;
71
81
  });
72
- inst[key] = boundFn;
73
- });
82
+ proto = Object.getPrototypeOf(proto);
83
+ }
74
84
 
75
85
  ctrlMap[ctrlName] = inst;
76
86
  }
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 / createLogger"]
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` / `createLogger`、`Paths`、`safePath`、`validate` / `validateAll` | 日常工具 |
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 / createLogger / Paths / 文件安全 / validate / utils 工具集
1
+ # 05 · 工具与校验:logger / Paths / 文件安全 / validate / utils 工具集
2
2
 
3
3
  日常开发最常用的小工具。导出方式分两类:
4
- - **根包直接导出**:`logger`(默认导出)、`createLogger`、`Paths`、`validate`、`validateAll`。
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` / `createLogger`
10
+ ## 一、日志 `logger`
11
11
 
12
- 文件:`utils/logger.js`。统一日志,**dev 彩色控制台、prd JSON 单行**(方便日志采集)。
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 | 错误(自动走 stderr)|
33
+ | `error` | 4 | 错误 |
32
34
  | `fatal` | 5 | 致命 |
33
35
 
34
- **级别控制**:环境变量 `LOG_LEVEL`(dev 默认 `debug`,prd 默认 `info`)。低于该级别的日志不输出。
35
- **输出目标**:基于 pino,dev pino-pretty 彩色控制台;prd 输出纯 JSON stdout,由 pm2 统一捕获落盘(框架不直接写日志文件)。
36
+ **级别控制**:唯一环境变量 `LOG_LEVEL`(默认 `info`),低于该级别的日志不输出——不读 `NODE_ENV`,级别只看这一个变量。
37
+ **输出目标**:pino + pino-pretty,统一 stdout。本地开发直接看控制台;线上由 pm2 捕获 stdout 落盘(框架不写日志文件、不接自定义管道)。格式固定为「时间戳 + 正文」,`pid/hostname/module/level` 固定头已忽略。
36
38
 
37
- ### 1.2 自定义标签实例 `createLogger(name)`
39
+ ### 1.2 子日志 / 链路日志
38
40
 
39
- 框架内部统一用默认 `logger`(标签 `Chan`)。**业务代码建议用 `createLogger` 自建带标签实例**,方便区分来源:
41
+ 需要区分来源或携带上下文时,用 pino 原生 `child`(不要再造 logger 实例):
40
42
 
41
43
  ```js
42
- import { createLogger } from "chanjs";
44
+ import { logger } from "chanjs";
43
45
 
44
- const log = createLogger("Member"); // 标签 Member
45
- log.info("用户登录", { userId: 1 });
46
- // 输出(pino-pretty 彩色):[2026-08-01 12:00:00.123 INFO]: 用户登录 {"userId":1}
46
+ const reqLog = logger.child({ userId: 1 }); // 绑定字段,每行自动附带
47
+ reqLog.info("用户登录");
48
+ // [14:09:33]: 用户登录 {"userId":1}
47
49
  ```
48
50
 
49
- > 📌 约定:框架内部统一 `import logger`,业务侧用 `createLogger("模块名")` 区分来源。dev 环境下 `module` 字段已被 pino-pretty 隐藏以精简输出(prod 仍保留便于聚合)。
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`(默认) | 实例 | 框架级日志(标签 Chan)|
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` 是默认导出**:`import logger from "chanjs"`(无花括号);`createLogger` 才是具名导出。
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, createLogger } from "./utils/logger.js";
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 { randomUUID } from "crypto";
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
- * 请求日志中间件(pino-http)
8
- *
9
- * 自动为每个请求生成 requestId,挂载 req.log(带 requestId 的子 logger)。
10
- * 业务 Controller 中可通过 req.log.info("xxx") 输出带 requestId 的日志。
11
- *
12
- * @param {object} app - Express 实例
13
- * @param {object} [config={}] - 配置对象
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "chanjs",
4
- "version": "2.7.17",
4
+ "version": "2.8.1",
5
5
  "description": "chanjs基于 Node.js + Express 5 的标准 HMVC 框架(NHMVC),纯 JavaScript(ESM)开发。",
6
6
  "main": "index.js",
7
7
  "module": "index.js",
package/utils/logger.js CHANGED
@@ -1,86 +1,22 @@
1
1
  import pino from "pino";
2
-
3
- const NODE_ENV = (process.env.NODE_ENV || "dev").toLowerCase();
4
- const IS_PROD = ["prd", "prod", "production"].includes(NODE_ENV);
5
- const LOG_LEVEL = process.env.LOG_LEVEL || (IS_PROD ? "info" : "debug");
6
-
7
- /**
8
- * pino 基础配置
9
- * - dev:pino-pretty 彩色输出,单行易读
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
- if (!IS_PROD) {
18
- baseOptions.transport = {
19
- target: "pino-pretty",
20
- options: {
21
- colorize: true,
22
- translateTime: "yyyy-mm-dd HH:mm:ss.l",
23
- // dev 终端只做单进程开发,module 恒为 "Chan" 无区分意义,隐藏以精简输出
24
- // (prod 仍保留 module 字段,便于日志聚合时按模块过滤)
25
- ignore: "pid,hostname,module",
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;