@lark-apaas/nestjs-logger 1.0.0 → 1.0.1-alpha.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 +38 -1
- package/dist/index.cjs +43 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +43 -35
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
- 独立的 trace 日志文件
|
|
12
12
|
- 请求上下文传递
|
|
13
13
|
- 日志体长度截断
|
|
14
|
+
- **完美兼容 Exception Filter**:
|
|
15
|
+
- 拦截 `response.json()` 和 `response.send()` 方法
|
|
16
|
+
- 能够捕获 Exception Filter 改写的响应体和最终状态码
|
|
17
|
+
- 确保异常日志的完整性和准确性
|
|
14
18
|
|
|
15
19
|
## 安装
|
|
16
20
|
|
|
@@ -95,6 +99,14 @@ HTTP 请求追踪日志使用 **`verbose` 级别**(对应 Pino 的 `trace` 级
|
|
|
95
99
|
- **需要显式启用**:要查看 HTTP 请求追踪日志,需要将 `LOGGER_LEVEL` 设置为 `verbose`
|
|
96
100
|
- **细粒度控制**:可以通过日志级别控制是否打印请求追踪,而无需修改 `LOG_REQUEST_BODY` 和 `LOG_RESPONSE_BODY` 配置
|
|
97
101
|
|
|
102
|
+
### 响应体记录规则
|
|
103
|
+
|
|
104
|
+
**只针对 JSON 响应记录 body**:
|
|
105
|
+
- 自动检测 `Content-Type` 是否包含 `application/json`
|
|
106
|
+
- 只有 JSON 响应才会记录 `response_body` 字段
|
|
107
|
+
- 文件下载、HTML 页面等非 JSON 响应不会记录 body,避免日志污染
|
|
108
|
+
- 这样可以显著减少日志体积,同时保持日志的可读性
|
|
109
|
+
|
|
98
110
|
### 启用方式
|
|
99
111
|
|
|
100
112
|
通过环境变量启用:
|
|
@@ -117,7 +129,11 @@ LOG_RESPONSE_BODY=true
|
|
|
117
129
|
|
|
118
130
|
2. **默认不限制长度**:如果不设置 `LOG_MAX_BODY_LENGTH` 环境变量,日志体不会被截断。建议在生产环境设置合理的限制值。
|
|
119
131
|
|
|
120
|
-
3.
|
|
132
|
+
3. **响应体过滤**:
|
|
133
|
+
- 只记录 JSON 响应的 body(检测 Content-Type)
|
|
134
|
+
- 文件下载、HTML、图片等非 JSON 响应不会记录 body
|
|
135
|
+
- 避免记录无法序列化的数据(如 Buffer、Stream)
|
|
136
|
+
- 显著减少日志体积,提高日志可读性
|
|
121
137
|
|
|
122
138
|
### 日志输出示例
|
|
123
139
|
|
|
@@ -229,6 +245,27 @@ NestJS LoggerService 级别到 Pino 级别的映射:
|
|
|
229
245
|
|
|
230
246
|
## 注意事项
|
|
231
247
|
|
|
248
|
+
### 架构设计
|
|
249
|
+
|
|
250
|
+
1. **Exception Filter 兼容性**:
|
|
251
|
+
- 使用 `response.on('finish')` 事件记录日志,确保在响应完全发送后才记录
|
|
252
|
+
- 拦截 `response.json()` 和 `response.send()` 方法,捕获最终发送的响应体
|
|
253
|
+
- 能够正确捕获全局 Exception Filter 设置的最终状态码和响应体
|
|
254
|
+
- 日志记录发生在 NestJS 请求生命周期的最后阶段,确保信息完整准确
|
|
255
|
+
|
|
256
|
+
2. **请求生命周期**:
|
|
257
|
+
```
|
|
258
|
+
Middleware → Guards → Interceptor (before) → Pipes → Controller
|
|
259
|
+
↓
|
|
260
|
+
Interceptor (after/catchError) → Exception Filters → Response Finish (✅ 日志记录)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
3. **实现原理**:
|
|
264
|
+
- **拦截响应方法**:在 Interceptor 中拦截 `res.json()` 和 `res.send()`,存储响应体到 `res.__finalResponseBody`
|
|
265
|
+
- **类型检测**:通过 `Content-Type` header 判断是否为 JSON 响应,只记录 JSON 类型的 body
|
|
266
|
+
- **延迟记录**:不在 Interceptor 的 `tap()` 或 `catchError()` 中记录日志,而是在 `finish` 事件中统一记录
|
|
267
|
+
- **完整信息**:此时可以获取 Exception Filter 处理后的最终状态码和响应体,确保日志准确性
|
|
268
|
+
|
|
232
269
|
### 安全性
|
|
233
270
|
|
|
234
271
|
1. **生产环境默认不打印 HTTP 追踪日志**:由于使用 verbose 级别,生产环境(info 级别)默认不会输出详细日志
|
package/dist/index.cjs
CHANGED
|
@@ -10567,7 +10567,7 @@ var require_tap = __commonJS({
|
|
|
10567
10567
|
var lift_1 = require_lift();
|
|
10568
10568
|
var OperatorSubscriber_1 = require_OperatorSubscriber();
|
|
10569
10569
|
var identity_1 = require_identity();
|
|
10570
|
-
function
|
|
10570
|
+
function tap(observerOrNext, error, complete) {
|
|
10571
10571
|
var tapObserver = isFunction_1.isFunction(observerOrNext) || error || complete ? {
|
|
10572
10572
|
next: observerOrNext,
|
|
10573
10573
|
error,
|
|
@@ -10600,8 +10600,8 @@ var require_tap = __commonJS({
|
|
|
10600
10600
|
}));
|
|
10601
10601
|
}) : identity_1.identity;
|
|
10602
10602
|
}
|
|
10603
|
-
__name(
|
|
10604
|
-
exports2.tap =
|
|
10603
|
+
__name(tap, "tap");
|
|
10604
|
+
exports2.tap = tap;
|
|
10605
10605
|
}
|
|
10606
10606
|
});
|
|
10607
10607
|
|
|
@@ -14855,6 +14855,16 @@ var LoggingInterceptor = class {
|
|
|
14855
14855
|
ip: req.ip ?? null,
|
|
14856
14856
|
pid: process.pid
|
|
14857
14857
|
};
|
|
14858
|
+
const originalJson = res.json.bind(res);
|
|
14859
|
+
const originalSend = res.send.bind(res);
|
|
14860
|
+
res.json = function(body) {
|
|
14861
|
+
res.__finalResponseBody = body;
|
|
14862
|
+
return originalJson(body);
|
|
14863
|
+
};
|
|
14864
|
+
res.send = function(body) {
|
|
14865
|
+
res.__finalResponseBody = body;
|
|
14866
|
+
return originalSend(body);
|
|
14867
|
+
};
|
|
14858
14868
|
const requestMeta = {
|
|
14859
14869
|
...baseMeta
|
|
14860
14870
|
};
|
|
@@ -14866,47 +14876,45 @@ var LoggingInterceptor = class {
|
|
|
14866
14876
|
}
|
|
14867
14877
|
this.appLogger.log("HTTP request started \n url=%s \n request_query=%o \n request_body=%o", req.url, requestMeta["query_params"] ?? {}, requestMeta["request_body"] ?? {});
|
|
14868
14878
|
this.traceLogger.logStructured("verbose", "HTTP request started", requestMeta, "HTTPTraceInterceptor");
|
|
14869
|
-
|
|
14879
|
+
let logged = false;
|
|
14880
|
+
res.on("finish", () => {
|
|
14881
|
+
if (logged) {
|
|
14882
|
+
return;
|
|
14883
|
+
}
|
|
14884
|
+
logged = true;
|
|
14870
14885
|
const durationMs = Date.now() - startedAt;
|
|
14871
14886
|
const statusCode = res.statusCode;
|
|
14872
|
-
const appLog = this.appLogger[res.statusCode >= 400 ? "error" : "log"].bind(this.appLogger);
|
|
14873
14887
|
const responseMeta = {
|
|
14874
14888
|
...baseMeta,
|
|
14875
14889
|
status_code: statusCode,
|
|
14876
14890
|
duration_ms: durationMs
|
|
14877
14891
|
};
|
|
14878
|
-
|
|
14879
|
-
|
|
14880
|
-
|
|
14881
|
-
|
|
14882
|
-
|
|
14883
|
-
|
|
14884
|
-
|
|
14885
|
-
const
|
|
14886
|
-
const
|
|
14887
|
-
const
|
|
14888
|
-
|
|
14889
|
-
|
|
14890
|
-
|
|
14891
|
-
|
|
14892
|
-
|
|
14893
|
-
|
|
14894
|
-
}
|
|
14895
|
-
const meta = {
|
|
14896
|
-
...baseMeta,
|
|
14897
|
-
status_code: statusCode,
|
|
14898
|
-
duration_ms: durationMs
|
|
14899
|
-
};
|
|
14900
|
-
if (error instanceof Error) {
|
|
14901
|
-
meta["error"] = {
|
|
14902
|
-
message: error.message,
|
|
14903
|
-
stack: error.stack
|
|
14904
|
-
};
|
|
14892
|
+
const finalResponseBody = res.__finalResponseBody;
|
|
14893
|
+
const contentType = res.getHeader("content-type");
|
|
14894
|
+
const isJsonResponse = typeof contentType === "string" && contentType.includes("application/json");
|
|
14895
|
+
if (this.config.logResponseBody && finalResponseBody !== void 0 && isJsonResponse) {
|
|
14896
|
+
responseMeta["response_body"] = this.sanitizeAndTruncate(finalResponseBody);
|
|
14897
|
+
}
|
|
14898
|
+
const isError = statusCode >= 400;
|
|
14899
|
+
const appLog = this.appLogger[isError ? "error" : "log"].bind(this.appLogger);
|
|
14900
|
+
const traceLevel = isError ? "error" : "verbose";
|
|
14901
|
+
const shouldLogBody = this.config.logResponseBody && isJsonResponse && finalResponseBody !== void 0;
|
|
14902
|
+
if (isError) {
|
|
14903
|
+
if (shouldLogBody) {
|
|
14904
|
+
appLog("HTTP request failed\n url=%s \n status_code=%d \n duration_ms=%d \n response_body=%o", req.url, statusCode, durationMs, finalResponseBody);
|
|
14905
|
+
} else {
|
|
14906
|
+
appLog("HTTP request failed\n url=%s \n status_code=%d \n duration_ms=%d", req.url, statusCode, durationMs);
|
|
14907
|
+
}
|
|
14905
14908
|
} else {
|
|
14906
|
-
|
|
14909
|
+
if (shouldLogBody) {
|
|
14910
|
+
appLog("HTTP request completed\n url=%s \n status_code=%d \n duration_ms=%d \n response_body=%o", req.url, statusCode, durationMs, finalResponseBody);
|
|
14911
|
+
} else {
|
|
14912
|
+
appLog("HTTP request completed\n url=%s \n status_code=%d \n duration_ms=%d", req.url, statusCode, durationMs);
|
|
14913
|
+
}
|
|
14907
14914
|
}
|
|
14908
|
-
this.
|
|
14909
|
-
|
|
14915
|
+
this.traceLogger.logStructured(traceLevel, isError ? "HTTP request failed" : "HTTP request completed", responseMeta, "HTTPTraceInterceptor");
|
|
14916
|
+
});
|
|
14917
|
+
return next.handle().pipe((0, import_operators.catchError)((error) => {
|
|
14910
14918
|
throw error;
|
|
14911
14919
|
}));
|
|
14912
14920
|
}
|