@ryanzeng/nest-observe 0.1.3 → 0.1.5
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 +102 -5
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +90 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +90 -16
- package/dist/index.mjs.map +1 -1
- package/dist/register.js +90 -16
- package/dist/register.js.map +1 -1
- package/dist/register.mjs +90 -16
- package/dist/register.mjs.map +1 -1
- package/package.json +10 -12
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+

|
|
2
2
|
|
|
3
3
|
面向 NestJS 的零配置、Vendor Neutral Observability SDK。基于 OpenTelemetry,通过标准 OTLP 同时发送 Traces、Metrics 和 Logs,可连接 OpenObserve、Grafana、Jaeger、Datadog、Elastic 等兼容后端。
|
|
4
4
|
|
|
@@ -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,45 @@ 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 `ConsoleLogger` 的重载解析语义,保留 `error(message, stack?, context?)`、附加消息和结构化参数
|
|
170
|
+
- `Error` 或显式 stack 会映射为 OTLP `exception.type`、`exception.message`、`exception.stacktrace` 属性
|
|
171
|
+
- 尊重 Nest 11+ 的实例级日志级别过滤(`logLevels` / `setLogLevels` / `Logger.overrideLogger`),上传内容与控制台输出一致
|
|
172
|
+
- 对象和数组消息会在脱敏后保持结构化 body;附加的结构化参数会展开为 OTLP attributes,同时保持首个 message 为 body
|
|
101
173
|
- OTLP severity、`nestjs.context`、`trace_id`、`span_id`
|
|
102
174
|
- `service.name`、`service.version`、`deployment.environment.name`
|
|
103
|
-
|
|
175
|
+
|
|
176
|
+
**采集边界**:自动桥接覆盖所有经过 `ConsoleLogger` 的日志(包括默认 `Logger`)。自定义 `LoggerService` 若完全不经过 `ConsoleLogger`(例如直接封装 Pino/Winston),不会被自动采集。两种接入方式:
|
|
177
|
+
|
|
178
|
+
一是用 `StructuredLogEmitter` 桥接,在自定义 Logger 里显式 emit:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { Injectable, LoggerService } from '@nestjs/common';
|
|
182
|
+
import { OpenTelemetryLogEmitter, redact } from '@ryanzeng/nest-observe';
|
|
183
|
+
|
|
184
|
+
@Injectable()
|
|
185
|
+
export class CustomLogger implements LoggerService {
|
|
186
|
+
private readonly emitter = new OpenTelemetryLogEmitter();
|
|
187
|
+
|
|
188
|
+
error(message: unknown, context?: string): void {
|
|
189
|
+
// 继续执行你的 Pino/Winston 输出……
|
|
190
|
+
this.emitter.emit({
|
|
191
|
+
body: redact(message),
|
|
192
|
+
severityText: 'ERROR',
|
|
193
|
+
attributes: context ? { 'nestjs.context': context } : {},
|
|
194
|
+
timestamp: Date.now(),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
二是注册 OTel 官方的 `@opentelemetry/instrumentation-pino` 或 `@opentelemetry/instrumentation-winston`(方式同上方"可选 Instrumentation"),日志会使用 SDK 注册的全局 Logger Provider,进入同一条 OTLP 管道。
|
|
104
201
|
|
|
105
202
|
Metrics:
|
|
106
203
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { LogRecordExporter, LoggerProvider } from '@opentelemetry/sdk-logs';
|
|
2
2
|
import { MetricReader, MeterProvider } from '@opentelemetry/sdk-metrics';
|
|
3
3
|
import { SpanExporter, SpanProcessor, ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
4
|
-
import {
|
|
5
|
-
import { Logger } from '@opentelemetry/api-logs';
|
|
4
|
+
import { LogAttributes, Logger } from '@opentelemetry/api-logs';
|
|
6
5
|
import { InstrumentationConfig, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
|
|
7
6
|
import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core';
|
|
7
|
+
import { Meter, Tracer, Span, Context } from '@opentelemetry/api';
|
|
8
8
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
9
9
|
import { NestInterceptor, ExecutionContext, CallHandler, DynamicModule } from '@nestjs/common';
|
|
10
10
|
import { Observable } from 'rxjs';
|
|
@@ -109,7 +109,7 @@ declare function Trace(nameOrOptions?: string | TraceOptions): TraceDecorator;
|
|
|
109
109
|
interface StructuredLogRecord {
|
|
110
110
|
body: unknown;
|
|
111
111
|
severityText: 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL';
|
|
112
|
-
attributes:
|
|
112
|
+
attributes: LogAttributes;
|
|
113
113
|
timestamp?: number;
|
|
114
114
|
}
|
|
115
115
|
interface StructuredLogEmitter {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { LogRecordExporter, LoggerProvider } from '@opentelemetry/sdk-logs';
|
|
2
2
|
import { MetricReader, MeterProvider } from '@opentelemetry/sdk-metrics';
|
|
3
3
|
import { SpanExporter, SpanProcessor, ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
4
|
-
import {
|
|
5
|
-
import { Logger } from '@opentelemetry/api-logs';
|
|
4
|
+
import { LogAttributes, Logger } from '@opentelemetry/api-logs';
|
|
6
5
|
import { InstrumentationConfig, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
|
|
7
6
|
import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core';
|
|
7
|
+
import { Meter, Tracer, Span, Context } from '@opentelemetry/api';
|
|
8
8
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
9
9
|
import { NestInterceptor, ExecutionContext, CallHandler, DynamicModule } from '@nestjs/common';
|
|
10
10
|
import { Observable } from 'rxjs';
|
|
@@ -109,7 +109,7 @@ declare function Trace(nameOrOptions?: string | TraceOptions): TraceDecorator;
|
|
|
109
109
|
interface StructuredLogRecord {
|
|
110
110
|
body: unknown;
|
|
111
111
|
severityText: 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL';
|
|
112
|
-
attributes:
|
|
112
|
+
attributes: LogAttributes;
|
|
113
113
|
timestamp?: number;
|
|
114
114
|
}
|
|
115
115
|
interface StructuredLogEmitter {
|
package/dist/index.js
CHANGED
|
@@ -347,6 +347,71 @@ function bodyValue(message) {
|
|
|
347
347
|
return redactText(message.stack ?? `${message.name}: ${message.message}`);
|
|
348
348
|
return redact(message);
|
|
349
349
|
}
|
|
350
|
+
function isPlainObject(value) {
|
|
351
|
+
if (value === null || typeof value !== "object") return false;
|
|
352
|
+
const prototype = Object.getPrototypeOf(value);
|
|
353
|
+
if (prototype === null) return true;
|
|
354
|
+
const constructor = Object.prototype.hasOwnProperty.call(prototype, "constructor") ? prototype.constructor : void 0;
|
|
355
|
+
return typeof constructor === "function" && constructor instanceof constructor && Function.prototype.toString.call(constructor) === Function.prototype.toString.call(Object);
|
|
356
|
+
}
|
|
357
|
+
function isStackFormat(value) {
|
|
358
|
+
return typeof value === "string" && /^(.)+\n\s+at .+:\d+:\d+/.test(value);
|
|
359
|
+
}
|
|
360
|
+
function fallbackMessages(logger, values) {
|
|
361
|
+
if (values.length <= 1) return { messages: values, context: logger.context };
|
|
362
|
+
const lastValue = values.at(-1);
|
|
363
|
+
const contextName = typeof lastValue === "string" ? lastValue : logger.context;
|
|
364
|
+
const remaining = typeof lastValue === "string" ? values.slice(0, -1) : values;
|
|
365
|
+
if (logger.options?.structuredParams === false) {
|
|
366
|
+
return { messages: remaining, context: contextName };
|
|
367
|
+
}
|
|
368
|
+
const messages = [remaining[0]];
|
|
369
|
+
const structuredParams = [];
|
|
370
|
+
for (const value of remaining.slice(1)) {
|
|
371
|
+
if (isPlainObject(value)) structuredParams.push(value);
|
|
372
|
+
else messages.push(value);
|
|
373
|
+
}
|
|
374
|
+
const params = structuredParams.length > 0 ? Object.assign({}, ...structuredParams) : void 0;
|
|
375
|
+
return { messages, context: contextName, ...params ? { params } : {} };
|
|
376
|
+
}
|
|
377
|
+
function fallbackError(logger, values) {
|
|
378
|
+
if (values.length === 2) {
|
|
379
|
+
if (isStackFormat(values[1])) {
|
|
380
|
+
return { messages: [values[0]], stack: values[1], context: logger.context };
|
|
381
|
+
}
|
|
382
|
+
return fallbackMessages(logger, values);
|
|
383
|
+
}
|
|
384
|
+
const trailingValue = values.at(-1);
|
|
385
|
+
if (isStackFormat(trailingValue)) {
|
|
386
|
+
return { ...fallbackMessages(logger, values.slice(0, -1)), stack: trailingValue };
|
|
387
|
+
}
|
|
388
|
+
const parsed = fallbackMessages(logger, values);
|
|
389
|
+
if (parsed.messages.length <= 1) return parsed;
|
|
390
|
+
const possibleStack = parsed.messages.at(-1);
|
|
391
|
+
if (typeof possibleStack !== "string" && possibleStack !== void 0) return parsed;
|
|
392
|
+
return {
|
|
393
|
+
...parsed,
|
|
394
|
+
messages: parsed.messages.slice(0, -1),
|
|
395
|
+
...possibleStack === void 0 ? {} : { stack: possibleStack }
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function parseLogCall(logger, method, message, args) {
|
|
399
|
+
const values = [message, ...args];
|
|
400
|
+
const nestParser = method === "error" ? logger.getContextAndStackAndMessagesToPrint : logger.getContextAndMessagesToPrint;
|
|
401
|
+
if (typeof nestParser === "function") return nestParser.call(logger, values);
|
|
402
|
+
return method === "error" ? fallbackError(logger, values) : fallbackMessages(logger, values);
|
|
403
|
+
}
|
|
404
|
+
function exceptionAttributes(message, stack) {
|
|
405
|
+
const error = message instanceof Error ? message : void 0;
|
|
406
|
+
const stacktrace = stack ?? error?.stack;
|
|
407
|
+
return {
|
|
408
|
+
...error ? {
|
|
409
|
+
"exception.type": error.name,
|
|
410
|
+
"exception.message": redactText(error.message)
|
|
411
|
+
} : {},
|
|
412
|
+
...stacktrace ? { "exception.stacktrace": redactText(stacktrace) } : {}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
350
415
|
var NestLoggerInstrumentation = class {
|
|
351
416
|
constructor(emitter, resourceAttributes = {}) {
|
|
352
417
|
this.emitter = emitter;
|
|
@@ -368,18 +433,30 @@ var NestLoggerInstrumentation = class {
|
|
|
368
433
|
const resourceAttributes = this.resourceAttributes;
|
|
369
434
|
prototype[method] = function(message, ...args) {
|
|
370
435
|
try {
|
|
436
|
+
const gated = this;
|
|
437
|
+
if (typeof gated.isLevelEnabled === "function" && !gated.isLevelEnabled(method)) {
|
|
438
|
+
return original.call(this, message, ...args);
|
|
439
|
+
}
|
|
371
440
|
const spanContext = import_api3.trace.getActiveSpan()?.spanContext();
|
|
372
|
-
const
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
441
|
+
const parsed = parseLogCall(this, method, message, args);
|
|
442
|
+
const timestamp = Date.now();
|
|
443
|
+
for (const parsedMessage of parsed.messages) {
|
|
444
|
+
const params = parsed.params ? redact(parsed.params) : {};
|
|
445
|
+
const attributes = {
|
|
446
|
+
...params,
|
|
447
|
+
...resourceAttributes,
|
|
448
|
+
...exceptionAttributes(parsedMessage, parsed.stack)
|
|
449
|
+
};
|
|
450
|
+
if (parsed.context) attributes["nestjs.context"] = parsed.context;
|
|
451
|
+
if (spanContext?.traceId) attributes.trace_id = spanContext.traceId;
|
|
452
|
+
if (spanContext?.spanId) attributes.span_id = spanContext.spanId;
|
|
453
|
+
emitter.emit({
|
|
454
|
+
body: bodyValue(parsedMessage),
|
|
455
|
+
severityText: SEVERITY[method],
|
|
456
|
+
attributes,
|
|
457
|
+
timestamp
|
|
458
|
+
});
|
|
459
|
+
}
|
|
383
460
|
} catch {
|
|
384
461
|
}
|
|
385
462
|
return original.call(this, message, ...args);
|
|
@@ -697,7 +774,6 @@ var import_exporter_metrics_otlp_proto = require("@opentelemetry/exporter-metric
|
|
|
697
774
|
var import_exporter_trace_otlp_proto = require("@opentelemetry/exporter-trace-otlp-proto");
|
|
698
775
|
var import_instrumentation2 = require("@opentelemetry/instrumentation");
|
|
699
776
|
var import_instrumentation_http = require("@opentelemetry/instrumentation-http");
|
|
700
|
-
var import_instrumentation3 = require("@prisma/instrumentation");
|
|
701
777
|
var import_sdk_logs = require("@opentelemetry/sdk-logs");
|
|
702
778
|
var import_sdk_metrics = require("@opentelemetry/sdk-metrics");
|
|
703
779
|
var import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
|
|
@@ -863,7 +939,7 @@ var import_node_os2 = require("os");
|
|
|
863
939
|
// package.json
|
|
864
940
|
var package_default = {
|
|
865
941
|
name: "@ryanzeng/nest-observe",
|
|
866
|
-
version: "0.1.
|
|
942
|
+
version: "0.1.4",
|
|
867
943
|
description: "Zero-config, vendor-neutral OpenTelemetry observability for NestJS",
|
|
868
944
|
keywords: [
|
|
869
945
|
"nestjs",
|
|
@@ -948,8 +1024,7 @@ var package_default = {
|
|
|
948
1024
|
"@opentelemetry/sdk-logs": "^0.221.0",
|
|
949
1025
|
"@opentelemetry/sdk-metrics": "^2.10.0",
|
|
950
1026
|
"@opentelemetry/sdk-trace-base": "^2.10.0",
|
|
951
|
-
"@opentelemetry/sdk-trace-node": "^2.10.0"
|
|
952
|
-
"@prisma/instrumentation": "^7.10.0"
|
|
1027
|
+
"@opentelemetry/sdk-trace-node": "^2.10.0"
|
|
953
1028
|
},
|
|
954
1029
|
devDependencies: {
|
|
955
1030
|
"@nestjs/common": "^12.0.1",
|
|
@@ -1264,7 +1339,6 @@ function observe(options = {}) {
|
|
|
1264
1339
|
providerTracing: config.providerTracing,
|
|
1265
1340
|
controllerTracing: config.controllerTracing
|
|
1266
1341
|
}));
|
|
1267
|
-
if (config.traces) instrumentations.push(new import_instrumentation3.PrismaInstrumentation());
|
|
1268
1342
|
(0, import_instrumentation2.registerInstrumentations)({
|
|
1269
1343
|
instrumentations,
|
|
1270
1344
|
...tracerProvider ? { tracerProvider } : {},
|