@rare0619/nestjs-tracing 1.2.0
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/LICENSE +191 -0
- package/README.md +212 -0
- package/dist/decorators/span.decorator.d.ts +23 -0
- package/dist/decorators/span.decorator.d.ts.map +1 -0
- package/dist/decorators/span.decorator.js +69 -0
- package/dist/decorators/span.decorator.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/trace-sync.middleware.d.ts +24 -0
- package/dist/middleware/trace-sync.middleware.d.ts.map +1 -0
- package/dist/middleware/trace-sync.middleware.js +46 -0
- package/dist/middleware/trace-sync.middleware.js.map +1 -0
- package/dist/tracer.d.ts +46 -0
- package/dist/tracer.d.ts.map +1 -0
- package/dist/tracer.js +172 -0
- package/dist/tracer.js.map +1 -0
- package/dist/tracing.module.d.ts +21 -0
- package/dist/tracing.module.d.ts.map +1 -0
- package/dist/tracing.module.js +40 -0
- package/dist/tracing.module.js.map +1 -0
- package/dist/winston/otel-log-format.d.ts +29 -0
- package/dist/winston/otel-log-format.d.ts.map +1 -0
- package/dist/winston/otel-log-format.js +75 -0
- package/dist/winston/otel-log-format.js.map +1 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to the Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by the Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding any notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
Copyright 2026 rare0619
|
|
180
|
+
|
|
181
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
182
|
+
you may not use this file except in compliance with the License.
|
|
183
|
+
You may obtain a copy of the License at
|
|
184
|
+
|
|
185
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
186
|
+
|
|
187
|
+
Unless required by applicable law or agreed to in writing, software
|
|
188
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
189
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
190
|
+
See the License for the specific language governing permissions and
|
|
191
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# @rare0619/nestjs-tracing
|
|
2
|
+
|
|
3
|
+
NestJS 全链路追踪插件 — 基于 OpenTelemetry,兼容 Elastic APM / Jaeger / 任何 OTLP 后端。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- **零配置启动**:`main.ts` 第一行 import 即可,自动推断服务名
|
|
8
|
+
- **白名单插桩**:仅启用项目实际使用的插桩库(HTTP、Express、PostgreSQL、Redis),避免冗余开销
|
|
9
|
+
- **NestJS 适配**:修复 NestJS + Express 场景下 transaction name 只显示 `GET`/`HEAD` 的问题,自动设为 `METHOD /path` 格式
|
|
10
|
+
- **噪音过滤**:自动过滤 health check(incoming)和 ES/APM/OTLP 基础设施请求(outgoing),保持 trace waterfall 干净
|
|
11
|
+
- **日志关联**:提供 `otelLogFormat()` winston format,自动注入 `trace.id` / `span.id`
|
|
12
|
+
- **手动打点**:`@Span()` 装饰器支持自定义 span
|
|
13
|
+
- **优雅关机**:SIGTERM/SIGINT 时自动 flush 缓冲区中的 span
|
|
14
|
+
|
|
15
|
+
## 快速开始
|
|
16
|
+
|
|
17
|
+
### 1. 安装
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @rare0619/nestjs-tracing
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 2. 初始化(main.ts 第一行)
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// ⚠️ 必须是第一行 import,在 @nestjs/core 之前
|
|
27
|
+
import '@rare0619/nestjs-tracing/tracer';
|
|
28
|
+
|
|
29
|
+
import { NestFactory } from '@nestjs/core';
|
|
30
|
+
import { AppModule } from './app.module';
|
|
31
|
+
|
|
32
|
+
async function bootstrap() {
|
|
33
|
+
const app = await NestFactory.create(AppModule);
|
|
34
|
+
await app.listen(3000);
|
|
35
|
+
}
|
|
36
|
+
bootstrap();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
> **为什么必须第一行**:OTel 通过 monkey-patch Node.js 的 `http`、`pg`、`ioredis` 等模块实现插桩。如果这些模块在 OTel 初始化之前被 `require()`,patch 将无效。
|
|
40
|
+
|
|
41
|
+
### 3. 环境变量
|
|
42
|
+
|
|
43
|
+
| 变量 | 默认值 | 说明 |
|
|
44
|
+
|------|--------|------|
|
|
45
|
+
| `OTEL_SERVICE_NAME` | 自动推断 | 服务名(优先级:此变量 > `SERVICE_NAME` > 从 `dist/apps/{name}/main.js` 路径推断) |
|
|
46
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4318` | APM Server / OTLP Collector 地址(自动追加 `/v1/traces`) |
|
|
47
|
+
| `OTEL_TRACES_SAMPLER_ARG` | `1.0` | 采样率(开发环境 `1.0` 100%,生产环境建议 `0.1` 10%) |
|
|
48
|
+
| `OTEL_SDK_DISABLED` | — | 设为 `true` 禁用 SDK(无 APM Server 时避免报错) |
|
|
49
|
+
| `NODE_ENV` | `development` | 部署环境标识(写入 `deployment.environment` resource attribute) |
|
|
50
|
+
| `ELASTICSEARCH_PORT` | `9200` | ES 端口(用于 outgoing 请求过滤) |
|
|
51
|
+
| `APM_PORT` | `8200` | APM Server 端口(用于 outgoing 请求过滤) |
|
|
52
|
+
|
|
53
|
+
tracer 会自动加载项目根目录的 `.env` 文件(无需 `dotenv` 依赖)。
|
|
54
|
+
|
|
55
|
+
## 插桩覆盖
|
|
56
|
+
|
|
57
|
+
采用**白名单模式**,仅启用项目实际使用的插桩库:
|
|
58
|
+
|
|
59
|
+
| 插桩库 | 覆盖组件 | 说明 |
|
|
60
|
+
|--------|---------|------|
|
|
61
|
+
| `HttpInstrumentation` | HTTP 请求(含 axios 出站) | 含 incoming/outgoing 过滤和 server span 重命名 |
|
|
62
|
+
| `ExpressInstrumentation` | Express 路由(NestJS 底层) | |
|
|
63
|
+
| `PgInstrumentation` | PostgreSQL(TypeORM 底层驱动 `pg`) | trace 中可见 `pg.query:SELECT` |
|
|
64
|
+
| `IORedisInstrumentation` | Redis(`ioredis`) | trace 中可见 `sismember`/`get` 等 |
|
|
65
|
+
|
|
66
|
+
> **关于 axios / typeorm**:无需单独插桩——`axios` 底层走 `http` 模块,`typeorm` 底层走 `pg` 驱动,已被自动追踪。
|
|
67
|
+
|
|
68
|
+
### 噪音过滤
|
|
69
|
+
|
|
70
|
+
#### Incoming 过滤(`ignoreIncomingRequestHook`)
|
|
71
|
+
|
|
72
|
+
自动忽略 K8s 健康检查等高频低价值请求:
|
|
73
|
+
- URL 包含 `/health` 或 `/readiness`
|
|
74
|
+
|
|
75
|
+
#### Outgoing 过滤(`ignoreOutgoingRequestHook`)
|
|
76
|
+
|
|
77
|
+
三重过滤策略,防止 ES/APM/OTLP 基础设施请求产生噪音 span:
|
|
78
|
+
|
|
79
|
+
1. **端口匹配**:ES(9200)、APM(8200)、OTLP(4317/4318)
|
|
80
|
+
2. **Path 匹配**:`/_cluster/`、`/_bulk`、`/_template/` 等 ES 内部 API
|
|
81
|
+
3. **URL 匹配**:href 中包含 `:9200` 或 `:8200`
|
|
82
|
+
|
|
83
|
+
#### NestJS Transaction Name 修复
|
|
84
|
+
|
|
85
|
+
NestJS + Express 场景下,`ExpressInstrumentation` 无法正确捕获路由 pattern,导致 transaction name 只显示 `GET` / `HEAD`。通过 `applyCustomAttributesOnSpan` 自动重命名为 `GET /user/v1/userinfo` 格式。
|
|
86
|
+
|
|
87
|
+
### 扩展插桩
|
|
88
|
+
|
|
89
|
+
如需添加更多插桩库,修改 `src/tracer.ts` 中的 `instrumentations` 数组:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// MySQL / Doris(MySQL 协议兼容)
|
|
93
|
+
npm install @opentelemetry/instrumentation-mysql2
|
|
94
|
+
// 在 tracer.ts 中添加:
|
|
95
|
+
const { MySQL2Instrumentation } = require('@opentelemetry/instrumentation-mysql2');
|
|
96
|
+
// instrumentations: [..., new MySQL2Instrumentation()]
|
|
97
|
+
|
|
98
|
+
// RabbitMQ
|
|
99
|
+
npm install @opentelemetry/instrumentation-amqplib
|
|
100
|
+
// 在 tracer.ts 中添加:
|
|
101
|
+
const { AmqplibInstrumentation } = require('@opentelemetry/instrumentation-amqplib');
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 日志关联
|
|
105
|
+
|
|
106
|
+
将 Winston 日志与 Trace 关联,实现 Kibana APM Transaction → Logs 一键跳转:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { otelLogFormat } from '@rare0619/nestjs-tracing';
|
|
110
|
+
|
|
111
|
+
const logger = winston.createLogger({
|
|
112
|
+
format: winston.format.combine(
|
|
113
|
+
otelLogFormat(), // ← 自动注入 trace.id / span.id
|
|
114
|
+
winston.format.json(),
|
|
115
|
+
),
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
> **winston-elasticsearch 用户注意**:如果使用 `winston-elasticsearch` 直写 ES,需要额外处理:
|
|
120
|
+
> 1. 创建自定义 ES Client 并禁用 `diagnostic.removeAllListeners()`(防止 `bulk`/`cluster.health` 噪音 span)
|
|
121
|
+
> 2. 用 `api.context.with(ROOT_CONTEXT, ...)` 包裹 `origLog()` 调用(防止 BulkWriter 定时 flush 成为请求 span 的子 span)
|
|
122
|
+
>
|
|
123
|
+
> 详见 [kibana-apm-tracing-guide.md](../doc/kibana-apm-tracing-guide.md) 中问题 6 和问题 7。
|
|
124
|
+
|
|
125
|
+
### 生产环境:Filebeat 采集日志
|
|
126
|
+
|
|
127
|
+
生产环境推荐用 Filebeat 采集 JSON 日志文件(解耦应用与 ES,避免性能影响):
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
# filebeat.yml
|
|
131
|
+
filebeat.inputs:
|
|
132
|
+
- type: log
|
|
133
|
+
enabled: true
|
|
134
|
+
paths:
|
|
135
|
+
- /app/logs/gateway-*.log
|
|
136
|
+
- /app/logs/user-service-*.log
|
|
137
|
+
- /app/logs/social-service-*.log
|
|
138
|
+
# JSON 解析:将日志字段提升到文档根级(trace.id 等字段需要在根级才能被 Kibana APM 关联)
|
|
139
|
+
json.keys_under_root: true
|
|
140
|
+
json.add_error_key: true
|
|
141
|
+
json.overwrite_keys: true
|
|
142
|
+
# 排除 error/exceptions 独立文件(避免重复采集)
|
|
143
|
+
exclude_files: ['error-.*', 'exceptions-.*', 'rejections-.*']
|
|
144
|
+
|
|
145
|
+
# 单独采集 error 日志
|
|
146
|
+
- type: log
|
|
147
|
+
enabled: true
|
|
148
|
+
paths:
|
|
149
|
+
- /app/logs/error-*.log
|
|
150
|
+
json.keys_under_root: true
|
|
151
|
+
json.add_error_key: true
|
|
152
|
+
json.overwrite_keys: true
|
|
153
|
+
fields:
|
|
154
|
+
log.level: error
|
|
155
|
+
fields_under_root: true
|
|
156
|
+
|
|
157
|
+
output.elasticsearch:
|
|
158
|
+
hosts: ["${ELASTICSEARCH_HOSTS:localhost:9200}"]
|
|
159
|
+
username: "${ELASTICSEARCH_USER:elastic}"
|
|
160
|
+
password: "${ELASTICSEARCH_PASS:changeme}"
|
|
161
|
+
index: "app-logs-%{+yyyy.MM.dd}"
|
|
162
|
+
|
|
163
|
+
setup.template.name: "app-logs"
|
|
164
|
+
setup.template.pattern: "app-logs-*"
|
|
165
|
+
setup.ilm.enabled: false
|
|
166
|
+
|
|
167
|
+
processors:
|
|
168
|
+
- drop_fields:
|
|
169
|
+
fields: ["agent", "ecs", "host", "input", "log.offset"]
|
|
170
|
+
ignore_missing: true
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
> **关键点**:`json.keys_under_root: true` 会把 JSON 日志中的 `trace.id` 字段提升到文档根级,这样 Kibana APM 的 Logs 标签才能通过 `trace.id` 自动关联日志。
|
|
174
|
+
|
|
175
|
+
## 手动打点
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { Span } from '@rare0619/nestjs-tracing';
|
|
179
|
+
|
|
180
|
+
export class OrderService {
|
|
181
|
+
@Span('create-order')
|
|
182
|
+
async createOrder(dto: CreateOrderDto) {
|
|
183
|
+
// 自动创建 Span,异常自动记录
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## 中间件(可选)
|
|
189
|
+
|
|
190
|
+
自动将 OTel traceId 写入 `x-request-id` / `x-trace-id` 响应头:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { TracingModule } from '@rare0619/nestjs-tracing';
|
|
194
|
+
|
|
195
|
+
@Module({
|
|
196
|
+
imports: [TracingModule],
|
|
197
|
+
})
|
|
198
|
+
export class AppModule {}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## 已知问题与注意事项
|
|
202
|
+
|
|
203
|
+
| 问题 | 说明 |
|
|
204
|
+
|------|------|
|
|
205
|
+
| `@elastic/elasticsearch` v8+ 噪音 | ES Client 内置 OTel diagnostic channel,会产生 `bulk`/`cluster.health` 噪音 span。需在创建 Client 时调用 `diagnostic.removeAllListeners()` |
|
|
206
|
+
| `NetInstrumentation` | 产生大量 `tcp.connect` 噪音 span,不建议启用 |
|
|
207
|
+
| `requestHook` vs `applyCustomAttributesOnSpan` | 前者只对 outgoing 请求生效,后者对 incoming/outgoing 都生效。NestJS 场景下 server span 重命名必须用后者 |
|
|
208
|
+
| webpack 打包兼容 | OTel 的 monkey-patch 在 webpack `target: 'node'` 下正常工作(NestJS CLI 默认配置) |
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
Apache-2.0
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 方法装饰器:自动为方法创建 OTel Span
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* - 方法调用时自动创建 active span
|
|
6
|
+
* - 异常自动 recordException + 标记 ERROR 状态
|
|
7
|
+
* - 方法结束时自动 end span
|
|
8
|
+
* - 同步方法不会被强制异步化(避免不必要的事件循环延迟)
|
|
9
|
+
*
|
|
10
|
+
* 使用方式:
|
|
11
|
+
* import { Span } from '@rare0619/nestjs-tracing';
|
|
12
|
+
*
|
|
13
|
+
* @Span('create-order')
|
|
14
|
+
* async createOrder(dto: CreateOrderDto) {
|
|
15
|
+
* // 自动创建名为 'create-order' 的 Span
|
|
16
|
+
* // 异常自动捕获
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* @Span() // 默认使用 类名.方法名
|
|
20
|
+
* async getUser(id: string) { ... }
|
|
21
|
+
*/
|
|
22
|
+
export declare function Span(name?: string): MethodDecorator;
|
|
23
|
+
//# sourceMappingURL=span.decorator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"span.decorator.d.ts","sourceRoot":"","sources":["../../src/decorators/span.decorator.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,eAAe,CAsDnD"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Span = Span;
|
|
4
|
+
const api_1 = require("@opentelemetry/api");
|
|
5
|
+
/**
|
|
6
|
+
* 方法装饰器:自动为方法创建 OTel Span
|
|
7
|
+
*
|
|
8
|
+
* 功能:
|
|
9
|
+
* - 方法调用时自动创建 active span
|
|
10
|
+
* - 异常自动 recordException + 标记 ERROR 状态
|
|
11
|
+
* - 方法结束时自动 end span
|
|
12
|
+
* - 同步方法不会被强制异步化(避免不必要的事件循环延迟)
|
|
13
|
+
*
|
|
14
|
+
* 使用方式:
|
|
15
|
+
* import { Span } from '@rare0619/nestjs-tracing';
|
|
16
|
+
*
|
|
17
|
+
* @Span('create-order')
|
|
18
|
+
* async createOrder(dto: CreateOrderDto) {
|
|
19
|
+
* // 自动创建名为 'create-order' 的 Span
|
|
20
|
+
* // 异常自动捕获
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* @Span() // 默认使用 类名.方法名
|
|
24
|
+
* async getUser(id: string) { ... }
|
|
25
|
+
*/
|
|
26
|
+
function Span(name) {
|
|
27
|
+
return (target, propertyKey, descriptor) => {
|
|
28
|
+
const originalMethod = descriptor.value;
|
|
29
|
+
const spanName = name || `${target.constructor.name}.${String(propertyKey)}`;
|
|
30
|
+
descriptor.value = function (...args) {
|
|
31
|
+
const tracer = api_1.trace.getTracer('@rare0619/nestjs-tracing');
|
|
32
|
+
return tracer.startActiveSpan(spanName, (span) => {
|
|
33
|
+
try {
|
|
34
|
+
const result = originalMethod.apply(this, args);
|
|
35
|
+
// 异步方法:返回 Promise
|
|
36
|
+
if (result instanceof Promise) {
|
|
37
|
+
return result.then((val) => {
|
|
38
|
+
span.end();
|
|
39
|
+
return val;
|
|
40
|
+
}, (err) => {
|
|
41
|
+
span.recordException(err);
|
|
42
|
+
span.setStatus({
|
|
43
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
44
|
+
message: err?.message || 'Unknown error',
|
|
45
|
+
});
|
|
46
|
+
span.end();
|
|
47
|
+
throw err;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// 同步方法:直接结束(不引入 async 开销)
|
|
51
|
+
span.end();
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
// 同步方法抛出异常
|
|
56
|
+
span.recordException(err);
|
|
57
|
+
span.setStatus({
|
|
58
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
59
|
+
message: err?.message || 'Unknown error',
|
|
60
|
+
});
|
|
61
|
+
span.end();
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
return descriptor;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=span.decorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"span.decorator.js","sourceRoot":"","sources":["../../src/decorators/span.decorator.ts"],"names":[],"mappings":";;AAuBA,oBAsDC;AA7ED,4CAA2D;AAE3D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,SAAgB,IAAI,CAAC,IAAa;IAC9B,OAAO,CACH,MAAW,EACX,WAA4B,EAC5B,UAA8B,EAChC,EAAE;QACA,MAAM,cAAc,GAAG,UAAU,CAAC,KAAK,CAAC;QACxC,MAAM,QAAQ,GACV,IAAI,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,IAAI,IAAI,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC;QAEhE,UAAU,CAAC,KAAK,GAAG,UAAqB,GAAG,IAAW;YAClD,MAAM,MAAM,GAAG,WAAK,CAAC,SAAS,CAAC,0BAA0B,CAAC,CAAC;YAE3D,OAAO,MAAM,CAAC,eAAe,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE;gBAC7C,IAAI,CAAC;oBACD,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;oBAEhD,kBAAkB;oBAClB,IAAI,MAAM,YAAY,OAAO,EAAE,CAAC;wBAC5B,OAAO,MAAM,CAAC,IAAI,CACd,CAAC,GAAG,EAAE,EAAE;4BACJ,IAAI,CAAC,GAAG,EAAE,CAAC;4BACX,OAAO,GAAG,CAAC;wBACf,CAAC,EACD,CAAC,GAAG,EAAE,EAAE;4BACJ,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;4BAC1B,IAAI,CAAC,SAAS,CAAC;gCACX,IAAI,EAAE,oBAAc,CAAC,KAAK;gCAC1B,OAAO,EAAE,GAAG,EAAE,OAAO,IAAI,eAAe;6BAC3C,CAAC,CAAC;4BACH,IAAI,CAAC,GAAG,EAAE,CAAC;4BACX,MAAM,GAAG,CAAC;wBACd,CAAC,CACJ,CAAC;oBACN,CAAC;oBAED,0BAA0B;oBAC1B,IAAI,CAAC,GAAG,EAAE,CAAC;oBACX,OAAO,MAAM,CAAC;gBAClB,CAAC;gBAAC,OAAO,GAAQ,EAAE,CAAC;oBAChB,WAAW;oBACX,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;oBAC1B,IAAI,CAAC,SAAS,CAAC;wBACX,IAAI,EAAE,oBAAc,CAAC,KAAK;wBAC1B,OAAO,EAAE,GAAG,EAAE,OAAO,IAAI,eAAe;qBAC3C,CAAC,CAAC;oBACH,IAAI,CAAC,GAAG,EAAE,CAAC;oBACX,MAAM,GAAG,CAAC;gBACd,CAAC;YACL,CAAC,CAAC,CAAC;QACP,CAAC,CAAC;QAEF,OAAO,UAAU,CAAC;IACtB,CAAC,CAAC;AACN,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rare0619/nestjs-tracing
|
|
3
|
+
*
|
|
4
|
+
* NestJS 全链路追踪插件(OpenTelemetry + Elastic APM)
|
|
5
|
+
*
|
|
6
|
+
* 快速开始:
|
|
7
|
+
* // main.ts 第一行(必须在所有 import 之前)
|
|
8
|
+
* import '@rare0619/nestjs-tracing/tracer';
|
|
9
|
+
*
|
|
10
|
+
* // 日志关联(winston format 链)
|
|
11
|
+
* import { otelLogFormat } from '@rare0619/nestjs-tracing';
|
|
12
|
+
*
|
|
13
|
+
* // 手动打点装饰器
|
|
14
|
+
* import { Span } from '@rare0619/nestjs-tracing';
|
|
15
|
+
*/
|
|
16
|
+
export { otelLogFormat } from './winston/otel-log-format';
|
|
17
|
+
export { TraceSyncMiddleware } from './middleware/trace-sync.middleware';
|
|
18
|
+
export { Span } from './decorators/span.decorator';
|
|
19
|
+
export { TracingModule } from './tracing.module';
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAG1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAGzE,OAAO,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAC;AAGnD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @rare0619/nestjs-tracing
|
|
4
|
+
*
|
|
5
|
+
* NestJS 全链路追踪插件(OpenTelemetry + Elastic APM)
|
|
6
|
+
*
|
|
7
|
+
* 快速开始:
|
|
8
|
+
* // main.ts 第一行(必须在所有 import 之前)
|
|
9
|
+
* import '@rare0619/nestjs-tracing/tracer';
|
|
10
|
+
*
|
|
11
|
+
* // 日志关联(winston format 链)
|
|
12
|
+
* import { otelLogFormat } from '@rare0619/nestjs-tracing';
|
|
13
|
+
*
|
|
14
|
+
* // 手动打点装饰器
|
|
15
|
+
* import { Span } from '@rare0619/nestjs-tracing';
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.TracingModule = exports.Span = exports.TraceSyncMiddleware = exports.otelLogFormat = void 0;
|
|
19
|
+
// 日志关联
|
|
20
|
+
var otel_log_format_1 = require("./winston/otel-log-format");
|
|
21
|
+
Object.defineProperty(exports, "otelLogFormat", { enumerable: true, get: function () { return otel_log_format_1.otelLogFormat; } });
|
|
22
|
+
// 中间件
|
|
23
|
+
var trace_sync_middleware_1 = require("./middleware/trace-sync.middleware");
|
|
24
|
+
Object.defineProperty(exports, "TraceSyncMiddleware", { enumerable: true, get: function () { return trace_sync_middleware_1.TraceSyncMiddleware; } });
|
|
25
|
+
// 装饰器
|
|
26
|
+
var span_decorator_1 = require("./decorators/span.decorator");
|
|
27
|
+
Object.defineProperty(exports, "Span", { enumerable: true, get: function () { return span_decorator_1.Span; } });
|
|
28
|
+
// NestJS Module
|
|
29
|
+
var tracing_module_1 = require("./tracing.module");
|
|
30
|
+
Object.defineProperty(exports, "TracingModule", { enumerable: true, get: function () { return tracing_module_1.TracingModule; } });
|
|
31
|
+
// 注意:tracer.ts 不在此导出
|
|
32
|
+
// 必须通过 import '@rare0619/nestjs-tracing/tracer' 在 main.ts 第一行单独导入
|
|
33
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAEH,OAAO;AACP,6DAA0D;AAAjD,gHAAA,aAAa,OAAA;AAEtB,MAAM;AACN,4EAAyE;AAAhE,4HAAA,mBAAmB,OAAA;AAE5B,MAAM;AACN,8DAAmD;AAA1C,sGAAA,IAAI,OAAA;AAEb,gBAAgB;AAChB,mDAAiD;AAAxC,+GAAA,aAAa,OAAA;AAEtB,qBAAqB;AACrB,kEAAkE"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NestMiddleware } from '@nestjs/common';
|
|
2
|
+
import { Request, Response, NextFunction } from 'express';
|
|
3
|
+
/**
|
|
4
|
+
* 中间件:将 OTel traceId 同步到 HTTP 响应头
|
|
5
|
+
*
|
|
6
|
+
* 功能:
|
|
7
|
+
* 1. 从 OTel active span 提取 W3C 标准 traceId (32 位 hex)
|
|
8
|
+
* 2. 写入 x-request-id 和 x-trace-id 响应头
|
|
9
|
+
* 3. 兼容已有的 x-request-id 机制(向后兼容)
|
|
10
|
+
*
|
|
11
|
+
* 使用方式:
|
|
12
|
+
* import { TraceSyncMiddleware } from '@rare0619/nestjs-tracing';
|
|
13
|
+
*
|
|
14
|
+
* // 在 AppModule 中注册
|
|
15
|
+
* export class AppModule implements NestModule {
|
|
16
|
+
* configure(consumer: MiddlewareConsumer) {
|
|
17
|
+
* consumer.apply(TraceSyncMiddleware).forRoutes('*');
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
export declare class TraceSyncMiddleware implements NestMiddleware {
|
|
22
|
+
use(req: Request, res: Response, next: NextFunction): void;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=trace-sync.middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-sync.middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/trace-sync.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG1D;;;;;;;;;;;;;;;;;GAiBG;AACH,qBACa,mBAAoB,YAAW,cAAc;IACtD,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI;CAU7D"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.TraceSyncMiddleware = void 0;
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
|
+
const api_1 = require("@opentelemetry/api");
|
|
12
|
+
/**
|
|
13
|
+
* 中间件:将 OTel traceId 同步到 HTTP 响应头
|
|
14
|
+
*
|
|
15
|
+
* 功能:
|
|
16
|
+
* 1. 从 OTel active span 提取 W3C 标准 traceId (32 位 hex)
|
|
17
|
+
* 2. 写入 x-request-id 和 x-trace-id 响应头
|
|
18
|
+
* 3. 兼容已有的 x-request-id 机制(向后兼容)
|
|
19
|
+
*
|
|
20
|
+
* 使用方式:
|
|
21
|
+
* import { TraceSyncMiddleware } from '@rare0619/nestjs-tracing';
|
|
22
|
+
*
|
|
23
|
+
* // 在 AppModule 中注册
|
|
24
|
+
* export class AppModule implements NestModule {
|
|
25
|
+
* configure(consumer: MiddlewareConsumer) {
|
|
26
|
+
* consumer.apply(TraceSyncMiddleware).forRoutes('*');
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
let TraceSyncMiddleware = class TraceSyncMiddleware {
|
|
31
|
+
use(req, res, next) {
|
|
32
|
+
const activeSpan = api_1.trace.getActiveSpan();
|
|
33
|
+
if (activeSpan) {
|
|
34
|
+
const traceId = activeSpan.spanContext().traceId;
|
|
35
|
+
// 向后兼容旧 x-request-id + 新标准 x-trace-id
|
|
36
|
+
res.header('x-request-id', traceId);
|
|
37
|
+
res.header('x-trace-id', traceId);
|
|
38
|
+
}
|
|
39
|
+
next();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
exports.TraceSyncMiddleware = TraceSyncMiddleware;
|
|
43
|
+
exports.TraceSyncMiddleware = TraceSyncMiddleware = __decorate([
|
|
44
|
+
(0, common_1.Injectable)()
|
|
45
|
+
], TraceSyncMiddleware);
|
|
46
|
+
//# sourceMappingURL=trace-sync.middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-sync.middleware.js","sourceRoot":"","sources":["../../src/middleware/trace-sync.middleware.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA4D;AAE5D,4CAA2C;AAE3C;;;;;;;;;;;;;;;;;GAiBG;AAEI,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAC5B,GAAG,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;QAC/C,MAAM,UAAU,GAAG,WAAK,CAAC,aAAa,EAAE,CAAC;QACzC,IAAI,UAAU,EAAE,CAAC;YACb,MAAM,OAAO,GAAG,UAAU,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC;YACjD,sCAAsC;YACtC,GAAG,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;YACpC,GAAG,CAAC,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QACtC,CAAC;QACD,IAAI,EAAE,CAAC;IACX,CAAC;CACJ,CAAA;AAXY,kDAAmB;8BAAnB,mBAAmB;IAD/B,IAAA,mBAAU,GAAE;GACA,mBAAmB,CAW/B"}
|
package/dist/tracer.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rare0619/nestjs-tracing — OTel SDK 自动初始化
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ 必须在 main.ts 第一行 import(在任何 NestJS/Express 导入之前)
|
|
5
|
+
*
|
|
6
|
+
* 使用方式:
|
|
7
|
+
* import '@rare0619/nestjs-tracing/tracer';
|
|
8
|
+
* import { NestFactory } from '@nestjs/core';
|
|
9
|
+
*
|
|
10
|
+
* 环境变量(均为可选,有智能默认值):
|
|
11
|
+
* OTEL_SERVICE_NAME 服务名(不设则从入口路径自动推断)
|
|
12
|
+
* OTEL_EXPORTER_OTLP_ENDPOINT APM Server 地址(默认 http://localhost:4318/v1/traces)
|
|
13
|
+
* OTEL_TRACES_SAMPLER 采样器(默认 parentbased_traceidratio)
|
|
14
|
+
* OTEL_TRACES_SAMPLER_ARG 采样率(开发 1.0 / 生产 0.1)
|
|
15
|
+
* OTEL_SDK_DISABLED 设为 'true' 禁用 SDK(无 APM 时使用)
|
|
16
|
+
* NODE_ENV 环境标识
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* 智能推断服务名
|
|
20
|
+
*
|
|
21
|
+
* 优先级:
|
|
22
|
+
* 1. OTEL_SERVICE_NAME 环境变量(显式指定)
|
|
23
|
+
* 2. SERVICE_NAME 环境变量(Docker/K8s 注入)
|
|
24
|
+
* 3. 从入口路径自动推断(NestJS monorepo: dist/apps/{service-name}/main.js)
|
|
25
|
+
* 4. 'unknown-service' 兜底
|
|
26
|
+
*/
|
|
27
|
+
export declare function detectServiceName(): string;
|
|
28
|
+
/**
|
|
29
|
+
* 构建 OTLP Traces 完整 URL
|
|
30
|
+
* OTLPTraceExporter 的 `url` 参数不会自动追加 /v1/traces,需手动处理
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildTracesUrl(endpoint?: string): string;
|
|
33
|
+
/** 判断 incoming 请求是否应被忽略(health check 等高频低价值请求) */
|
|
34
|
+
export declare function shouldIgnoreIncoming(url: string): boolean;
|
|
35
|
+
/** ES / APM 等基础设施的端口,用于过滤 outgoing 噪音 span */
|
|
36
|
+
export declare function getInfraPorts(): Set<number>;
|
|
37
|
+
/** ES 内部 API 路径前缀 */
|
|
38
|
+
export declare const ES_PATH_PREFIXES: string[];
|
|
39
|
+
/** 判断 outgoing 请求是否应被忽略(ES/APM/OTLP 基础设施请求) */
|
|
40
|
+
export declare function shouldIgnoreOutgoing(opts: {
|
|
41
|
+
port?: number | string;
|
|
42
|
+
path?: string;
|
|
43
|
+
href?: string;
|
|
44
|
+
hostname?: string;
|
|
45
|
+
}): boolean;
|
|
46
|
+
//# sourceMappingURL=tracer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracer.d.ts","sourceRoot":"","sources":["../src/tracer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAgCH;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAS1C;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAKxD;AAED,kDAAkD;AAClD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,8CAA8C;AAC9C,wBAAgB,aAAa,IAAI,GAAG,CAAC,MAAM,CAAC,CAO3C;AAED,qBAAqB;AACrB,eAAO,MAAM,gBAAgB,UAAiF,CAAC;AAE/G,+CAA+C;AAC/C,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAQ/H"}
|
package/dist/tracer.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @rare0619/nestjs-tracing — OTel SDK 自动初始化
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ 必须在 main.ts 第一行 import(在任何 NestJS/Express 导入之前)
|
|
6
|
+
*
|
|
7
|
+
* 使用方式:
|
|
8
|
+
* import '@rare0619/nestjs-tracing/tracer';
|
|
9
|
+
* import { NestFactory } from '@nestjs/core';
|
|
10
|
+
*
|
|
11
|
+
* 环境变量(均为可选,有智能默认值):
|
|
12
|
+
* OTEL_SERVICE_NAME 服务名(不设则从入口路径自动推断)
|
|
13
|
+
* OTEL_EXPORTER_OTLP_ENDPOINT APM Server 地址(默认 http://localhost:4318/v1/traces)
|
|
14
|
+
* OTEL_TRACES_SAMPLER 采样器(默认 parentbased_traceidratio)
|
|
15
|
+
* OTEL_TRACES_SAMPLER_ARG 采样率(开发 1.0 / 生产 0.1)
|
|
16
|
+
* OTEL_SDK_DISABLED 设为 'true' 禁用 SDK(无 APM 时使用)
|
|
17
|
+
* NODE_ENV 环境标识
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.ES_PATH_PREFIXES = void 0;
|
|
21
|
+
exports.detectServiceName = detectServiceName;
|
|
22
|
+
exports.buildTracesUrl = buildTracesUrl;
|
|
23
|
+
exports.shouldIgnoreIncoming = shouldIgnoreIncoming;
|
|
24
|
+
exports.getInfraPorts = getInfraPorts;
|
|
25
|
+
exports.shouldIgnoreOutgoing = shouldIgnoreOutgoing;
|
|
26
|
+
// ============ 可导出的纯函数(供单元测试使用)============
|
|
27
|
+
// 在 SDK 初始化之前先加载 .env
|
|
28
|
+
// (因为 tracer 在 main.ts 第一行执行,NestJS 的 dotenv 尚未加载)
|
|
29
|
+
// 使用 fs 手动解析,避免 webpack 打包对 require('dotenv') 的干扰
|
|
30
|
+
try {
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const envPath = path.resolve(process.cwd(), '.env');
|
|
34
|
+
if (fs.existsSync(envPath)) {
|
|
35
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
36
|
+
for (const line of envContent.split('\n')) {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
39
|
+
continue;
|
|
40
|
+
const eqIdx = trimmed.indexOf('=');
|
|
41
|
+
if (eqIdx === -1)
|
|
42
|
+
continue;
|
|
43
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
44
|
+
const value = trimmed.slice(eqIdx + 1).trim();
|
|
45
|
+
// 不覆盖已有的系统环境变量
|
|
46
|
+
if (!process.env[key]) {
|
|
47
|
+
process.env[key] = value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// 静默忽略:靠系统环境变量即可
|
|
54
|
+
}
|
|
55
|
+
// ============ 可导出的纯函数(供单元测试使用)============
|
|
56
|
+
/**
|
|
57
|
+
* 智能推断服务名
|
|
58
|
+
*
|
|
59
|
+
* 优先级:
|
|
60
|
+
* 1. OTEL_SERVICE_NAME 环境变量(显式指定)
|
|
61
|
+
* 2. SERVICE_NAME 环境变量(Docker/K8s 注入)
|
|
62
|
+
* 3. 从入口路径自动推断(NestJS monorepo: dist/apps/{service-name}/main.js)
|
|
63
|
+
* 4. 'unknown-service' 兜底
|
|
64
|
+
*/
|
|
65
|
+
function detectServiceName() {
|
|
66
|
+
if (process.env.OTEL_SERVICE_NAME)
|
|
67
|
+
return process.env.OTEL_SERVICE_NAME;
|
|
68
|
+
if (process.env.SERVICE_NAME)
|
|
69
|
+
return process.env.SERVICE_NAME;
|
|
70
|
+
const entryFile = process.argv[1] || '';
|
|
71
|
+
const match = entryFile.match(/dist[\/\\]apps[\/\\]([^\/\\]+)/);
|
|
72
|
+
if (match)
|
|
73
|
+
return match[1];
|
|
74
|
+
return 'unknown-service';
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 构建 OTLP Traces 完整 URL
|
|
78
|
+
* OTLPTraceExporter 的 `url` 参数不会自动追加 /v1/traces,需手动处理
|
|
79
|
+
*/
|
|
80
|
+
function buildTracesUrl(endpoint) {
|
|
81
|
+
const rawEndpoint = endpoint || 'http://localhost:4318';
|
|
82
|
+
return rawEndpoint.endsWith('/v1/traces')
|
|
83
|
+
? rawEndpoint
|
|
84
|
+
: `${rawEndpoint.replace(/\/+$/, '')}/v1/traces`;
|
|
85
|
+
}
|
|
86
|
+
/** 判断 incoming 请求是否应被忽略(health check 等高频低价值请求) */
|
|
87
|
+
function shouldIgnoreIncoming(url) {
|
|
88
|
+
return url.includes('/health') || url.includes('/readiness');
|
|
89
|
+
}
|
|
90
|
+
/** ES / APM 等基础设施的端口,用于过滤 outgoing 噪音 span */
|
|
91
|
+
function getInfraPorts() {
|
|
92
|
+
return new Set([
|
|
93
|
+
parseInt(process.env.ELASTICSEARCH_PORT || '9200', 10),
|
|
94
|
+
parseInt(process.env.APM_PORT || '8200', 10),
|
|
95
|
+
4317,
|
|
96
|
+
4318,
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
99
|
+
/** ES 内部 API 路径前缀 */
|
|
100
|
+
exports.ES_PATH_PREFIXES = ['/_cluster/', '/_bulk', '/_template/', '/_index_template/', '/_data_stream/'];
|
|
101
|
+
/** 判断 outgoing 请求是否应被忽略(ES/APM/OTLP 基础设施请求) */
|
|
102
|
+
function shouldIgnoreOutgoing(opts) {
|
|
103
|
+
const port = Number(opts.port);
|
|
104
|
+
if (port && getInfraPorts().has(port))
|
|
105
|
+
return true;
|
|
106
|
+
const reqPath = opts.path || '';
|
|
107
|
+
if (exports.ES_PATH_PREFIXES.some((prefix) => reqPath.startsWith(prefix)))
|
|
108
|
+
return true;
|
|
109
|
+
const href = opts.href || opts.hostname || '';
|
|
110
|
+
if (typeof href === 'string' && (href.includes(':9200') || href.includes(':8200')))
|
|
111
|
+
return true;
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
// ============ SDK 初始化(仅在未禁用时执行)============
|
|
115
|
+
if (process.env.OTEL_SDK_DISABLED !== 'true') {
|
|
116
|
+
const { NodeSDK } = require('@opentelemetry/sdk-node');
|
|
117
|
+
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-proto');
|
|
118
|
+
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
|
|
119
|
+
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');
|
|
120
|
+
const { PgInstrumentation } = require('@opentelemetry/instrumentation-pg');
|
|
121
|
+
const { IORedisInstrumentation } = require('@opentelemetry/instrumentation-ioredis');
|
|
122
|
+
const { Resource } = require('@opentelemetry/resources');
|
|
123
|
+
const { SEMRESATTRS_SERVICE_NAME, SEMRESATTRS_DEPLOYMENT_ENVIRONMENT, } = require('@opentelemetry/semantic-conventions');
|
|
124
|
+
const { W3CTraceContextPropagator } = require('@opentelemetry/core');
|
|
125
|
+
const serviceName = detectServiceName();
|
|
126
|
+
const tracesUrl = buildTracesUrl(process.env.OTEL_EXPORTER_OTLP_ENDPOINT);
|
|
127
|
+
const sdk = new NodeSDK({
|
|
128
|
+
resource: new Resource({
|
|
129
|
+
[SEMRESATTRS_SERVICE_NAME]: serviceName,
|
|
130
|
+
[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development',
|
|
131
|
+
}),
|
|
132
|
+
traceExporter: new OTLPTraceExporter({
|
|
133
|
+
url: tracesUrl,
|
|
134
|
+
timeoutMillis: 10000,
|
|
135
|
+
}),
|
|
136
|
+
textMapPropagator: new W3CTraceContextPropagator(),
|
|
137
|
+
instrumentations: [
|
|
138
|
+
// ——— HTTP & Express ———
|
|
139
|
+
new HttpInstrumentation({
|
|
140
|
+
ignoreIncomingRequestHook: (req) => shouldIgnoreIncoming(req.url || ''),
|
|
141
|
+
ignoreOutgoingRequestHook: (opts) => shouldIgnoreOutgoing(opts),
|
|
142
|
+
// 重命名 incoming server span:NestJS + Express 默认只有 METHOD,改为 METHOD /path
|
|
143
|
+
applyCustomAttributesOnSpan: (span, request, _response) => {
|
|
144
|
+
// IncomingMessage(server span)有 httpVersion 属性,ClientRequest(client span)没有
|
|
145
|
+
if (request?.httpVersion && request.method && request.url) {
|
|
146
|
+
const path = (request.url || '').split('?')[0];
|
|
147
|
+
span.updateName(`${request.method} ${path}`);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
new ExpressInstrumentation(),
|
|
152
|
+
// ——— 数据库 ———
|
|
153
|
+
new PgInstrumentation(), // PostgreSQL(TypeORM 底层驱动 pg)
|
|
154
|
+
new IORedisInstrumentation(), // Redis (ioredis)
|
|
155
|
+
// NetInstrumentation 产生大量 tcp.connect 噪音,不启用
|
|
156
|
+
// Doris 使用 MySQL 协议 — 项目中暂未安装 mysql/mysql2 驱动
|
|
157
|
+
// 如需启用:npm install @opentelemetry/instrumentation-mysql2
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
sdk.start();
|
|
161
|
+
console.log(`[@rare0619/tracing] initialized: service=${serviceName}, endpoint=${tracesUrl}`);
|
|
162
|
+
// 优雅关机:确保缓冲区中的 Span 被冲刷到网络
|
|
163
|
+
const shutdown = async () => {
|
|
164
|
+
await sdk.shutdown();
|
|
165
|
+
console.log('[@rare0619/tracing] shutdown complete');
|
|
166
|
+
};
|
|
167
|
+
process.on('SIGTERM', shutdown);
|
|
168
|
+
process.on('SIGINT', shutdown);
|
|
169
|
+
// @ts-ignore — 导出供外部访问(可选)
|
|
170
|
+
module.exports = { sdk };
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=tracer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracer.js","sourceRoot":"","sources":["../src/tracer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { MiddlewareConsumer, NestModule } from '@nestjs/common';
|
|
2
|
+
/**
|
|
3
|
+
* NestJS 追踪模块
|
|
4
|
+
*
|
|
5
|
+
* 导入此模块将自动注册 TraceSyncMiddleware(全局路由)
|
|
6
|
+
*
|
|
7
|
+
* 使用方式:
|
|
8
|
+
* import { TracingModule } from '@rare0619/nestjs-tracing';
|
|
9
|
+
*
|
|
10
|
+
* @Module({
|
|
11
|
+
* imports: [TracingModule],
|
|
12
|
+
* })
|
|
13
|
+
* export class AppModule {}
|
|
14
|
+
*
|
|
15
|
+
* 注意:tracer.ts 的 import 必须在 main.ts 第一行单独完成,
|
|
16
|
+
* 不能依赖 NestJS DI 容器(因为初始化必须早于所有模块加载)。
|
|
17
|
+
*/
|
|
18
|
+
export declare class TracingModule implements NestModule {
|
|
19
|
+
configure(consumer: MiddlewareConsumer): void;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=tracing.module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracing.module.d.ts","sourceRoot":"","sources":["../src/tracing.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,kBAAkB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGxE;;;;;;;;;;;;;;;GAeG;AACH,qBAIa,aAAc,YAAW,UAAU;IAC5C,SAAS,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI;CAGhD"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.TracingModule = void 0;
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
|
+
const trace_sync_middleware_1 = require("./middleware/trace-sync.middleware");
|
|
12
|
+
/**
|
|
13
|
+
* NestJS 追踪模块
|
|
14
|
+
*
|
|
15
|
+
* 导入此模块将自动注册 TraceSyncMiddleware(全局路由)
|
|
16
|
+
*
|
|
17
|
+
* 使用方式:
|
|
18
|
+
* import { TracingModule } from '@rare0619/nestjs-tracing';
|
|
19
|
+
*
|
|
20
|
+
* @Module({
|
|
21
|
+
* imports: [TracingModule],
|
|
22
|
+
* })
|
|
23
|
+
* export class AppModule {}
|
|
24
|
+
*
|
|
25
|
+
* 注意:tracer.ts 的 import 必须在 main.ts 第一行单独完成,
|
|
26
|
+
* 不能依赖 NestJS DI 容器(因为初始化必须早于所有模块加载)。
|
|
27
|
+
*/
|
|
28
|
+
let TracingModule = class TracingModule {
|
|
29
|
+
configure(consumer) {
|
|
30
|
+
consumer.apply(trace_sync_middleware_1.TraceSyncMiddleware).forRoutes('*');
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
exports.TracingModule = TracingModule;
|
|
34
|
+
exports.TracingModule = TracingModule = __decorate([
|
|
35
|
+
(0, common_1.Module)({
|
|
36
|
+
providers: [trace_sync_middleware_1.TraceSyncMiddleware],
|
|
37
|
+
exports: [trace_sync_middleware_1.TraceSyncMiddleware],
|
|
38
|
+
})
|
|
39
|
+
], TracingModule);
|
|
40
|
+
//# sourceMappingURL=tracing.module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracing.module.js","sourceRoot":"","sources":["../src/tracing.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwE;AACxE,8EAAyE;AAEzE;;;;;;;;;;;;;;;GAeG;AAKI,IAAM,aAAa,GAAnB,MAAM,aAAa;IACtB,SAAS,CAAC,QAA4B;QAClC,QAAQ,CAAC,KAAK,CAAC,2CAAmB,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACvD,CAAC;CACJ,CAAA;AAJY,sCAAa;wBAAb,aAAa;IAJzB,IAAA,eAAM,EAAC;QACJ,SAAS,EAAE,CAAC,2CAAmB,CAAC;QAChC,OAAO,EAAE,CAAC,2CAAmB,CAAC;KACjC,CAAC;GACW,aAAa,CAIzB"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as winston from 'winston';
|
|
2
|
+
/**
|
|
3
|
+
* Winston format:自动注入 OTel trace.id / span.id 到每条日志
|
|
4
|
+
*
|
|
5
|
+
* 注入字段(符合 Elastic Common Schema):
|
|
6
|
+
* trace.id — 32 位 hex,全局唯一链路 ID
|
|
7
|
+
* span.id — 16 位 hex,当前操作 Span ID
|
|
8
|
+
* trace.flags — 采样标记(1=已采样, 0=未采样)
|
|
9
|
+
*
|
|
10
|
+
* 使用方式:
|
|
11
|
+
* import { otelLogFormat } from '@rare0619/nestjs-tracing';
|
|
12
|
+
*
|
|
13
|
+
* const format = winston.format.combine(
|
|
14
|
+
* otelLogFormat(), // ← 加入 format 链即可
|
|
15
|
+
* winston.format.json(),
|
|
16
|
+
* );
|
|
17
|
+
*
|
|
18
|
+
* 输出效果:
|
|
19
|
+
* {
|
|
20
|
+
* "message": "GET /api/user 200",
|
|
21
|
+
* "trace.id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
|
|
22
|
+
* "span.id": "1234567890abcdef",
|
|
23
|
+
* "trace.flags": 1
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* Kibana APM 自动识别 trace.id → Transaction 详情页 "View Logs" 一键跳转
|
|
27
|
+
*/
|
|
28
|
+
export declare const otelLogFormat: winston.Logform.FormatWrap;
|
|
29
|
+
//# sourceMappingURL=otel-log-format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otel-log-format.d.ts","sourceRoot":"","sources":["../../src/winston/otel-log-format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AAGnC;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,aAAa,4BASxB,CAAC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.otelLogFormat = void 0;
|
|
37
|
+
const winston = __importStar(require("winston"));
|
|
38
|
+
const api_1 = require("@opentelemetry/api");
|
|
39
|
+
/**
|
|
40
|
+
* Winston format:自动注入 OTel trace.id / span.id 到每条日志
|
|
41
|
+
*
|
|
42
|
+
* 注入字段(符合 Elastic Common Schema):
|
|
43
|
+
* trace.id — 32 位 hex,全局唯一链路 ID
|
|
44
|
+
* span.id — 16 位 hex,当前操作 Span ID
|
|
45
|
+
* trace.flags — 采样标记(1=已采样, 0=未采样)
|
|
46
|
+
*
|
|
47
|
+
* 使用方式:
|
|
48
|
+
* import { otelLogFormat } from '@rare0619/nestjs-tracing';
|
|
49
|
+
*
|
|
50
|
+
* const format = winston.format.combine(
|
|
51
|
+
* otelLogFormat(), // ← 加入 format 链即可
|
|
52
|
+
* winston.format.json(),
|
|
53
|
+
* );
|
|
54
|
+
*
|
|
55
|
+
* 输出效果:
|
|
56
|
+
* {
|
|
57
|
+
* "message": "GET /api/user 200",
|
|
58
|
+
* "trace.id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
|
|
59
|
+
* "span.id": "1234567890abcdef",
|
|
60
|
+
* "trace.flags": 1
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* Kibana APM 自动识别 trace.id → Transaction 详情页 "View Logs" 一键跳转
|
|
64
|
+
*/
|
|
65
|
+
exports.otelLogFormat = winston.format((info) => {
|
|
66
|
+
const activeSpan = api_1.trace.getSpan(api_1.context.active());
|
|
67
|
+
if (activeSpan) {
|
|
68
|
+
const spanCtx = activeSpan.spanContext();
|
|
69
|
+
info['trace.id'] = spanCtx.traceId;
|
|
70
|
+
info['span.id'] = spanCtx.spanId;
|
|
71
|
+
info['trace.flags'] = spanCtx.traceFlags;
|
|
72
|
+
}
|
|
73
|
+
return info;
|
|
74
|
+
});
|
|
75
|
+
//# sourceMappingURL=otel-log-format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otel-log-format.js","sourceRoot":"","sources":["../../src/winston/otel-log-format.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,iDAAmC;AACnC,4CAAoD;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACU,QAAA,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;IACjD,MAAM,UAAU,GAAG,WAAK,CAAC,OAAO,CAAC,aAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACnD,IAAI,UAAU,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,UAAU,CAAC,WAAW,EAAE,CAAC;QACzC,IAAI,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;QACnC,IAAI,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;QACjC,IAAI,CAAC,aAAa,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC;IAC7C,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rare0619/nestjs-tracing",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "NestJS 全链路追踪插件(OpenTelemetry + Elastic APM)— 自动插桩、日志关联、跨服务传播、Monorepo 零配置",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./tracer": {
|
|
13
|
+
"types": "./dist/tracer.d.ts",
|
|
14
|
+
"default": "./dist/tracer.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"clean": "rm -rf dist",
|
|
20
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
21
|
+
"test": "jest --forceExit --detectOpenHandles",
|
|
22
|
+
"test:cov": "jest --coverage --forceExit --detectOpenHandles"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"nestjs",
|
|
26
|
+
"opentelemetry",
|
|
27
|
+
"tracing",
|
|
28
|
+
"elastic-apm",
|
|
29
|
+
"distributed-tracing",
|
|
30
|
+
"observability",
|
|
31
|
+
"monorepo"
|
|
32
|
+
],
|
|
33
|
+
"author": "rare0619",
|
|
34
|
+
"license": "Apache-2.0",
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@opentelemetry/api": "^1.9.0",
|
|
37
|
+
"winston": "^3.0.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"winston": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@opentelemetry/core": "^1.30.0",
|
|
46
|
+
"@opentelemetry/exporter-trace-otlp-proto": "^0.57.2",
|
|
47
|
+
"@opentelemetry/instrumentation-express": "^0.46.0",
|
|
48
|
+
"@opentelemetry/instrumentation-http": "^0.57.0",
|
|
49
|
+
"@opentelemetry/instrumentation-ioredis": "^0.61.0",
|
|
50
|
+
"@opentelemetry/instrumentation-pg": "^0.65.0",
|
|
51
|
+
"@opentelemetry/resources": "^1.30.0",
|
|
52
|
+
"@opentelemetry/sdk-node": "^0.57.0",
|
|
53
|
+
"@opentelemetry/semantic-conventions": "^1.30.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@nestjs/common": "^10.0.0",
|
|
57
|
+
"@opentelemetry/api": "^1.9.0",
|
|
58
|
+
"@types/express": "^4.17.21",
|
|
59
|
+
"@types/jest": "^29.5.0",
|
|
60
|
+
"@types/node": "^20.11.0",
|
|
61
|
+
"express": "^4.18.0",
|
|
62
|
+
"jest": "^29.7.0",
|
|
63
|
+
"reflect-metadata": "^0.2.0",
|
|
64
|
+
"ts-jest": "^29.2.0",
|
|
65
|
+
"typescript": "^5.3.0",
|
|
66
|
+
"winston": "^3.17.0"
|
|
67
|
+
},
|
|
68
|
+
"files": [
|
|
69
|
+
"dist",
|
|
70
|
+
"README.md"
|
|
71
|
+
]
|
|
72
|
+
}
|