@ryanzeng/nest-observe 0.1.2 → 0.1.4
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 +99 -4
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +20 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +20 -17
- package/dist/index.mjs.map +1 -1
- package/dist/register.js +13 -16
- package/dist/register.js.map +1 -1
- package/dist/register.mjs +13 -16
- package/dist/register.mjs.map +1 -1
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ OBSERVE_ENVIRONMENT=production
|
|
|
44
44
|
|
|
45
45
|
## Nest Module
|
|
46
46
|
|
|
47
|
-
`register` 已能在加载阶段自动挂载 HTTP、Nest Controller、Provider
|
|
47
|
+
`register` 已能在加载阶段自动挂载 HTTP、Nest Controller、Provider 和 Logger instrumentation。也可以导入全局 Module;它为较晚初始化或测试场景提供 Discovery fallback,并在 Nest 关闭时 flush/shutdown:
|
|
48
48
|
|
|
49
49
|
```ts
|
|
50
50
|
import { Module } from '@nestjs/common';
|
|
@@ -58,6 +58,75 @@ export class AppModule {}
|
|
|
58
58
|
|
|
59
59
|
Module-only 模式还会注册一个全局 Nest interceptor,补采因 HTTP 模块已经加载而无法挂载底层 hook 的已匹配 Controller 请求指标。它与早期 HTTP instrumentation 共享请求状态,不会重复计数。
|
|
60
60
|
|
|
61
|
+
## 可选 Instrumentation
|
|
62
|
+
|
|
63
|
+
核心包不内置 Prisma、Redis 或其他数据库客户端 instrumentation。应用只安装自己实际使用的适配包,并在目标客户端被加载之前注册:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Prisma
|
|
67
|
+
pnpm add @prisma/instrumentation
|
|
68
|
+
|
|
69
|
+
# node-redis
|
|
70
|
+
pnpm add @opentelemetry/instrumentation-redis
|
|
71
|
+
|
|
72
|
+
# ioredis
|
|
73
|
+
pnpm add @opentelemetry/instrumentation-ioredis
|
|
74
|
+
|
|
75
|
+
# Pino / Winston(日志类,见"自动采集内容 → Logs")
|
|
76
|
+
pnpm add @opentelemetry/instrumentation-pino
|
|
77
|
+
pnpm add @opentelemetry/instrumentation-winston
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
创建一个只负责注册应用级 instrumentation 的文件,例如 `src/observability.instrumentation.ts`:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { registerInstrumentations } from '@opentelemetry/instrumentation';
|
|
84
|
+
import { PrismaInstrumentation } from '@prisma/instrumentation';
|
|
85
|
+
import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis';
|
|
86
|
+
|
|
87
|
+
export const unregisterApplicationInstrumentations = registerInstrumentations({
|
|
88
|
+
instrumentations: [
|
|
89
|
+
new PrismaInstrumentation(),
|
|
90
|
+
new IORedisInstrumentation(),
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
如果应用使用的是 `redis` 包而不是 `ioredis`,替换为:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { RedisInstrumentation } from '@opentelemetry/instrumentation-redis';
|
|
99
|
+
|
|
100
|
+
new RedisInstrumentation();
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
然后将注册文件放在应用入口的最前面。使用早期 `register` 初始化时:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import './observability.instrumentation';
|
|
107
|
+
import '@ryanzeng/nest-observe/register';
|
|
108
|
+
import { NestFactory } from '@nestjs/core';
|
|
109
|
+
import { AppModule } from './app.module';
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
仅使用 `ObserveModule.forRoot()` 时同样先加载应用级 instrumentation:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import './observability.instrumentation';
|
|
116
|
+
import { NestFactory } from '@nestjs/core';
|
|
117
|
+
import { AppModule } from './app.module';
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
应用级 instrumentation 会使用 `nest-observe` 注册的全局 tracer provider,因此不应再创建第二个 `NodeTracerProvider` 或第二套 OTLP exporter。对于会重排 import 的 bundler,生产环境应使用 Node 的 `--require`(CommonJS)或 `--import`(ESM)预加载编译后的 instrumentation 文件,确保它早于 Prisma/Redis 客户端执行。
|
|
121
|
+
|
|
122
|
+
### 为什么必须放在入口第一条 import
|
|
123
|
+
|
|
124
|
+
OTel instrumentation 通过 hook Node 的模块加载器工作:当目标模块(如 `@prisma/client`)第一次被 require/import 时,hook 替换它的导出(`PrismaClient` 类等);模块一旦进入 `require.cache`,加载流程不会再走一遍,hook 永远不会再触发。事后补救也不可行——ESM 命名空间只读,CJS 下应用已持有的类引用也不受缓存修改影响。
|
|
125
|
+
|
|
126
|
+
而 import 的求值顺序是"先 imports、后模块体":`import { AppModule }` 会把整个依赖图(包括 `PrismaService` → `@prisma/client`)加载完,之后才执行 main.ts 的函数体。因此任何运行在 Nest DI 生命周期里的注册(包括 `ObserveModule.forRoot()`)都晚于客户端加载——这正是 `forRoot()` 不提供第三方客户端 instrumentation 参数的原因,只有 import 声明顺序的第一位早于它。Nest 语义层采集(Controller/Provider 方法)不受此限制:那些对象由 DI 在运行时逐个创建,天然晚于 instrumentation 就位。
|
|
127
|
+
|
|
128
|
+
OTel 官方对"注册过晚"的处理同样只是警告、无法补救(`_warnOnPreloadedModules`),所以放错位置的注册不会报错,只会静默失效——排查"span 没有出现"问题时,请先检查注册文件是否在入口第一条 import。
|
|
129
|
+
|
|
61
130
|
## 装饰器
|
|
62
131
|
|
|
63
132
|
```ts
|
|
@@ -90,17 +159,43 @@ export class OrderService {
|
|
|
90
159
|
Traces:
|
|
91
160
|
|
|
92
161
|
- Node HTTP client/server、Nest Controller/handler、Provider method
|
|
93
|
-
- Prisma instrumentation
|
|
162
|
+
- 应用可按需组合 Prisma、Redis、ioredis 等标准 OpenTelemetry instrumentation
|
|
94
163
|
- `@Trace()` 自定义 span 和自然 parent/child context
|
|
95
164
|
- Controller/Provider 异常事件、错误状态及 stack trace
|
|
96
165
|
|
|
97
166
|
Logs:
|
|
98
167
|
|
|
99
168
|
- 默认 Nest `Logger` 的 `verbose/debug/log/warn/error/fatal`
|
|
100
|
-
-
|
|
169
|
+
- 尊重 Nest 11+ 的实例级日志级别过滤(`logLevels` / `setLogLevels` / `Logger.overrideLogger`),上传内容与控制台输出一致
|
|
170
|
+
- 对象和数组消息会在脱敏后保持结构化 body,由观测后端或查询侧决定如何展开、展示或序列化
|
|
101
171
|
- OTLP severity、`nestjs.context`、`trace_id`、`span_id`
|
|
102
172
|
- `service.name`、`service.version`、`deployment.environment.name`
|
|
103
|
-
|
|
173
|
+
|
|
174
|
+
**采集边界**:自动桥接覆盖所有经过 `ConsoleLogger` 的日志(包括默认 `Logger`)。自定义 `LoggerService` 若完全不经过 `ConsoleLogger`(例如直接封装 Pino/Winston),不会被自动采集。两种接入方式:
|
|
175
|
+
|
|
176
|
+
一是用 `StructuredLogEmitter` 桥接,在自定义 Logger 里显式 emit:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { Injectable, LoggerService } from '@nestjs/common';
|
|
180
|
+
import { OpenTelemetryLogEmitter, redact } from '@ryanzeng/nest-observe';
|
|
181
|
+
|
|
182
|
+
@Injectable()
|
|
183
|
+
export class CustomLogger implements LoggerService {
|
|
184
|
+
private readonly emitter = new OpenTelemetryLogEmitter();
|
|
185
|
+
|
|
186
|
+
error(message: unknown, context?: string): void {
|
|
187
|
+
// 继续执行你的 Pino/Winston 输出……
|
|
188
|
+
this.emitter.emit({
|
|
189
|
+
body: redact(message),
|
|
190
|
+
severityText: 'ERROR',
|
|
191
|
+
attributes: context ? { 'nestjs.context': context } : {},
|
|
192
|
+
timestamp: Date.now(),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
二是注册 OTel 官方的 `@opentelemetry/instrumentation-pino` 或 `@opentelemetry/instrumentation-winston`(方式同上方"可选 Instrumentation"),日志会使用 SDK 注册的全局 Logger Provider,进入同一条 OTLP 管道。
|
|
104
199
|
|
|
105
200
|
Metrics:
|
|
106
201
|
|
package/dist/index.d.mts
CHANGED
|
@@ -177,8 +177,8 @@ declare class NestMethodInstrumenter {
|
|
|
177
177
|
private readonly errors;
|
|
178
178
|
private readonly instrumented;
|
|
179
179
|
constructor(tracer: Tracer, meter: Meter);
|
|
180
|
-
instrumentInstance(instance: object, kind: NestComponentKind, componentName?:
|
|
181
|
-
instrumentPrototype(type: Function, kind: NestComponentKind, componentName?:
|
|
180
|
+
instrumentInstance(instance: object, kind: NestComponentKind, componentName?: unknown): void;
|
|
181
|
+
instrumentPrototype(type: Function, kind: NestComponentKind, componentName?: unknown): void;
|
|
182
182
|
private instrumentTarget;
|
|
183
183
|
}
|
|
184
184
|
|
package/dist/index.d.ts
CHANGED
|
@@ -177,8 +177,8 @@ declare class NestMethodInstrumenter {
|
|
|
177
177
|
private readonly errors;
|
|
178
178
|
private readonly instrumented;
|
|
179
179
|
constructor(tracer: Tracer, meter: Meter);
|
|
180
|
-
instrumentInstance(instance: object, kind: NestComponentKind, componentName?:
|
|
181
|
-
instrumentPrototype(type: Function, kind: NestComponentKind, componentName?:
|
|
180
|
+
instrumentInstance(instance: object, kind: NestComponentKind, componentName?: unknown): void;
|
|
181
|
+
instrumentPrototype(type: Function, kind: NestComponentKind, componentName?: unknown): void;
|
|
182
182
|
private instrumentTarget;
|
|
183
183
|
}
|
|
184
184
|
|
package/dist/index.js
CHANGED
|
@@ -233,6 +233,11 @@ function invokeWithSpan(tracer, spanName, attributes, invoke, onFinish) {
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
// src/decorators/trace.decorator.ts
|
|
236
|
+
function copyOwnMetadata(source, target) {
|
|
237
|
+
for (const metadataKey of Reflect.getOwnMetadataKeys(source)) {
|
|
238
|
+
Reflect.defineMetadata(metadataKey, Reflect.getOwnMetadata(metadataKey, source), target);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
236
241
|
function decorateMethod(target, propertyKey, descriptor, options) {
|
|
237
242
|
const original = descriptor.value;
|
|
238
243
|
if (typeof original !== "function" || isTraceIgnored(original, target.constructor))
|
|
@@ -257,6 +262,7 @@ function decorateMethod(target, propertyKey, descriptor, options) {
|
|
|
257
262
|
value: original.name,
|
|
258
263
|
configurable: true
|
|
259
264
|
});
|
|
265
|
+
copyOwnMetadata(original, wrapped);
|
|
260
266
|
markTraceDecorated(wrapped);
|
|
261
267
|
descriptor.value = wrapped;
|
|
262
268
|
}
|
|
@@ -337,14 +343,9 @@ var SEVERITY = {
|
|
|
337
343
|
fatal: "FATAL"
|
|
338
344
|
};
|
|
339
345
|
function bodyValue(message) {
|
|
340
|
-
if (message instanceof Error)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
try {
|
|
344
|
-
return JSON.stringify(sanitized, (_key, value) => typeof value === "bigint" ? value.toString() : value);
|
|
345
|
-
} catch {
|
|
346
|
-
return redactText(String(sanitized));
|
|
347
|
-
}
|
|
346
|
+
if (message instanceof Error)
|
|
347
|
+
return redactText(message.stack ?? `${message.name}: ${message.message}`);
|
|
348
|
+
return redact(message);
|
|
348
349
|
}
|
|
349
350
|
var NestLoggerInstrumentation = class {
|
|
350
351
|
constructor(emitter, resourceAttributes = {}) {
|
|
@@ -367,6 +368,10 @@ var NestLoggerInstrumentation = class {
|
|
|
367
368
|
const resourceAttributes = this.resourceAttributes;
|
|
368
369
|
prototype[method] = function(message, ...args) {
|
|
369
370
|
try {
|
|
371
|
+
const gated = this;
|
|
372
|
+
if (typeof gated.isLevelEnabled === "function" && !gated.isLevelEnabled(method)) {
|
|
373
|
+
return original.call(this, message, ...args);
|
|
374
|
+
}
|
|
370
375
|
const spanContext = import_api3.trace.getActiveSpan()?.spanContext();
|
|
371
376
|
const contextName = this.context ?? (typeof args.at(-1) === "string" ? String(args.at(-1)) : void 0);
|
|
372
377
|
const attributes = { ...resourceAttributes };
|
|
@@ -388,7 +393,8 @@ var NestLoggerInstrumentation = class {
|
|
|
388
393
|
disable() {
|
|
389
394
|
if (!this.enabled) return;
|
|
390
395
|
const prototype = import_common.ConsoleLogger.prototype;
|
|
391
|
-
for (const [method, original] of this.originals)
|
|
396
|
+
for (const [method, original] of this.originals)
|
|
397
|
+
prototype[method] = original;
|
|
392
398
|
this.originals.clear();
|
|
393
399
|
this.enabled = false;
|
|
394
400
|
}
|
|
@@ -464,7 +470,7 @@ var NestMethodInstrumenter = class {
|
|
|
464
470
|
if (!descriptor || typeof original !== "function" || isTraceIgnored(original, type) || isTraceDecorated(original)) continue;
|
|
465
471
|
const existing = Reflect.get(target, methodName);
|
|
466
472
|
if (typeof existing === "function" && existing !== original && isTraceDecorated(existing)) continue;
|
|
467
|
-
const name = componentName
|
|
473
|
+
const name = typeof componentName === "symbol" ? String(componentName) : typeof componentName === "string" && componentName ? componentName : type.name || "Anonymous";
|
|
468
474
|
const attributes = {
|
|
469
475
|
[`nestjs.${kind}`]: name,
|
|
470
476
|
"nestjs.method": methodName
|
|
@@ -517,7 +523,7 @@ var CompatibleNestInstrumentation = class extends import_instrumentation_nestjs_
|
|
|
517
523
|
const instance = await original.apply(this, args);
|
|
518
524
|
try {
|
|
519
525
|
const wrapper = args[1];
|
|
520
|
-
if (!instance || typeof instance !== "object" || !wrapper?.metatype) return instance;
|
|
526
|
+
if (!instance || typeof instance !== "object" || !wrapper?.metatype || wrapper.inject !== void 0) return instance;
|
|
521
527
|
if (["InternalCoreModule", "ObserveModule", "DiscoveryModule"].includes(wrapper.host?.name ?? "")) {
|
|
522
528
|
return instance;
|
|
523
529
|
}
|
|
@@ -695,7 +701,6 @@ var import_exporter_metrics_otlp_proto = require("@opentelemetry/exporter-metric
|
|
|
695
701
|
var import_exporter_trace_otlp_proto = require("@opentelemetry/exporter-trace-otlp-proto");
|
|
696
702
|
var import_instrumentation2 = require("@opentelemetry/instrumentation");
|
|
697
703
|
var import_instrumentation_http = require("@opentelemetry/instrumentation-http");
|
|
698
|
-
var import_instrumentation3 = require("@prisma/instrumentation");
|
|
699
704
|
var import_sdk_logs = require("@opentelemetry/sdk-logs");
|
|
700
705
|
var import_sdk_metrics = require("@opentelemetry/sdk-metrics");
|
|
701
706
|
var import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
|
|
@@ -861,7 +866,7 @@ var import_node_os2 = require("os");
|
|
|
861
866
|
// package.json
|
|
862
867
|
var package_default = {
|
|
863
868
|
name: "@ryanzeng/nest-observe",
|
|
864
|
-
version: "0.1.
|
|
869
|
+
version: "0.1.4",
|
|
865
870
|
description: "Zero-config, vendor-neutral OpenTelemetry observability for NestJS",
|
|
866
871
|
keywords: [
|
|
867
872
|
"nestjs",
|
|
@@ -946,8 +951,7 @@ var package_default = {
|
|
|
946
951
|
"@opentelemetry/sdk-logs": "^0.221.0",
|
|
947
952
|
"@opentelemetry/sdk-metrics": "^2.10.0",
|
|
948
953
|
"@opentelemetry/sdk-trace-base": "^2.10.0",
|
|
949
|
-
"@opentelemetry/sdk-trace-node": "^2.10.0"
|
|
950
|
-
"@prisma/instrumentation": "^7.10.0"
|
|
954
|
+
"@opentelemetry/sdk-trace-node": "^2.10.0"
|
|
951
955
|
},
|
|
952
956
|
devDependencies: {
|
|
953
957
|
"@nestjs/common": "^12.0.1",
|
|
@@ -1262,7 +1266,6 @@ function observe(options = {}) {
|
|
|
1262
1266
|
providerTracing: config.providerTracing,
|
|
1263
1267
|
controllerTracing: config.controllerTracing
|
|
1264
1268
|
}));
|
|
1265
|
-
if (config.traces) instrumentations.push(new import_instrumentation3.PrismaInstrumentation());
|
|
1266
1269
|
(0, import_instrumentation2.registerInstrumentations)({
|
|
1267
1270
|
instrumentations,
|
|
1268
1271
|
...tracerProvider ? { tracerProvider } : {},
|
|
@@ -1367,7 +1370,7 @@ var NestObserveExplorer = class {
|
|
|
1367
1370
|
}
|
|
1368
1371
|
instrument(wrappers, instrumenter, kind) {
|
|
1369
1372
|
for (const wrapper of wrappers) {
|
|
1370
|
-
if (!wrapper.metatype) continue;
|
|
1373
|
+
if (!wrapper.metatype || wrapper.inject !== void 0) continue;
|
|
1371
1374
|
if (["ObserveModule", "DiscoveryModule", "InternalCoreModule"].includes(wrapper.host?.name ?? "")) continue;
|
|
1372
1375
|
if (wrapper.metatype === NestObserveExplorer) continue;
|
|
1373
1376
|
if (wrapper.instance) {
|