@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 CHANGED
@@ -44,7 +44,7 @@ OBSERVE_ENVIRONMENT=production
44
44
 
45
45
  ## Nest Module
46
46
 
47
- `register` 已能在加载阶段自动挂载 HTTP、Nest Controller、Provider、Prisma 和 Logger instrumentation。也可以导入全局 Module;它为较晚初始化或测试场景提供 Discovery fallback,并在 Nest 关闭时 flush/shutdown:
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
- - 对象和数组消息会在脱敏后序列化成 JSON 字符串,保证 OpenObserve 的标准 `body` 字段始终可见
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
- - `StructuredLogEmitter` 抽象可用于后续 Pino/Winston adapter
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?: string): void;
181
- instrumentPrototype(type: Function, kind: NestComponentKind, componentName?: string): void;
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?: string): void;
181
- instrumentPrototype(type: Function, kind: NestComponentKind, componentName?: string): void;
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) return redactText(message.stack ?? `${message.name}: ${message.message}`);
341
- const sanitized = redact(message);
342
- if (sanitized === null || typeof sanitized !== "object") return sanitized;
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) prototype[method] = original;
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 || type.name || "Anonymous";
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.2",
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) {