@biorate/nestjs-tools 2.4.0 → 2.5.2
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
|
@@ -8,7 +8,7 @@ Production‑grade middleware, guards, interceptors, filters, decorators, and HT
|
|
|
8
8
|
- **Response time** — Prometheus histogram + `x-response-time` header.
|
|
9
9
|
- **HTTP proxy** — Prometheus‑instrumented `http-proxy-middleware` wrapper.
|
|
10
10
|
- **Tracing** — OpenTelemetry span attributes for incoming requests.
|
|
11
|
-
- **Exception filter** — structured JSON error responses for HTTP
|
|
11
|
+
- **Exception filter** — structured JSON error responses for HTTP and WebSocket transports (RPC is unsupported).
|
|
12
12
|
- **Auth** — `@AuthBasic()` decorator + `AuthGuardProvider`.
|
|
13
13
|
- **Roles** — `@Roles()` decorator + `RolesGuardProvider`.
|
|
14
14
|
- **CORS** — config‑driven origin validation.
|
|
@@ -25,14 +25,14 @@ Peer dependencies: `@nestjs/common`, `@nestjs/core`, `@nestjs/swagger`, `class-v
|
|
|
25
25
|
|
|
26
26
|
## Quick start
|
|
27
27
|
|
|
28
|
+
`main.ts`:
|
|
29
|
+
|
|
28
30
|
```ts
|
|
29
31
|
import { NestFactory } from '@nestjs/core';
|
|
30
32
|
import { AppModule } from './app.module';
|
|
31
33
|
import {
|
|
32
34
|
AllExceptionsFilter,
|
|
33
|
-
|
|
34
|
-
RequestCountMiddleware,
|
|
35
|
-
ResponseTimeMiddleware,
|
|
35
|
+
RoutesInterceptor,
|
|
36
36
|
TracingInterceptor,
|
|
37
37
|
} from '@biorate/nestjs-tools';
|
|
38
38
|
|
|
@@ -40,17 +40,38 @@ async function bootstrap() {
|
|
|
40
40
|
const app = await NestFactory.create(AppModule, { bufferLogs: true });
|
|
41
41
|
|
|
42
42
|
app.useGlobalFilters(new AllExceptionsFilter());
|
|
43
|
-
app.useGlobalInterceptors(new TracingInterceptor());
|
|
44
|
-
|
|
45
|
-
// Middleware
|
|
46
|
-
app.use(BootstrapProvider.create());
|
|
47
|
-
app.use(RequestCountMiddleware.create());
|
|
48
|
-
app.use(ResponseTimeMiddleware.create());
|
|
43
|
+
app.useGlobalInterceptors(new RoutesInterceptor(), new TracingInterceptor());
|
|
49
44
|
|
|
50
45
|
await app.listen(3000);
|
|
51
46
|
}
|
|
47
|
+
|
|
48
|
+
bootstrap();
|
|
52
49
|
```
|
|
53
50
|
|
|
51
|
+
`app.module.ts`:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
|
55
|
+
import {
|
|
56
|
+
RequestCountMiddleware,
|
|
57
|
+
ResponseTimeMiddleware,
|
|
58
|
+
} from '@biorate/nestjs-tools';
|
|
59
|
+
|
|
60
|
+
@Module({ imports: [...] })
|
|
61
|
+
export class AppModule implements NestModule {
|
|
62
|
+
public configure(consumer: MiddlewareConsumer) {
|
|
63
|
+
consumer
|
|
64
|
+
.apply(RequestCountMiddleware, ResponseTimeMiddleware)
|
|
65
|
+
.forRoutes('*');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
> Middleware and interceptor classes use `@inject(Types.Config)` from the global
|
|
71
|
+
> `@biorate/inversion` container — bootstrap the app via the `@biorate/launcher`
|
|
72
|
+
> lifecycle with `Types.Config` bound, otherwise requests will fail with
|
|
73
|
+
> `InversionInjectionIsUndefinedError`.
|
|
74
|
+
|
|
54
75
|
## Middlewares
|
|
55
76
|
|
|
56
77
|
### `RequestCountMiddleware`
|
|
@@ -59,8 +80,6 @@ Prometheus counter `http_server_requests_seconds_count` with labels `method`, `u
|
|
|
59
80
|
|
|
60
81
|
```ts
|
|
61
82
|
consumer.apply(RequestCountMiddleware).forRoutes('*');
|
|
62
|
-
// or via BootstrapProvider
|
|
63
|
-
app.use(RequestCountMiddleware.create());
|
|
64
83
|
```
|
|
65
84
|
|
|
66
85
|
Uses `RoutesInterceptor` to resolve the actual route path (e.g. `/api/users/:id` instead of `/api/users/123`).
|
|
@@ -72,17 +91,17 @@ Config option: `app.middleware.RequestCountMiddleware.log-base-url` (boolean, de
|
|
|
72
91
|
Prometheus histogram `http_server_requests_seconds` + `x-response-time` header.
|
|
73
92
|
|
|
74
93
|
```ts
|
|
75
|
-
|
|
94
|
+
consumer.apply(ResponseTimeMiddleware).forRoutes('*');
|
|
76
95
|
```
|
|
77
96
|
|
|
78
97
|
Config options:
|
|
79
98
|
|
|
80
|
-
| Path
|
|
81
|
-
|
|
82
|
-
| `app.middleware.ResponseTimeMiddleware.suffix` | `true`
|
|
83
|
-
| `app.middleware.ResponseTimeMiddleware.header` | `x-response-time`
|
|
84
|
-
| `app.middleware.ResponseTimeMiddleware.digits` | `3`
|
|
85
|
-
| `app.middleware.ResponseTimeMiddleware.log-base-url` | `false`
|
|
99
|
+
| Path | Default |
|
|
100
|
+
| ---------------------------------------------------- | ----------------------- |
|
|
101
|
+
| `app.middleware.ResponseTimeMiddleware.suffix` | `true` |
|
|
102
|
+
| `app.middleware.ResponseTimeMiddleware.header` | `x-response-time` |
|
|
103
|
+
| `app.middleware.ResponseTimeMiddleware.digits` | `3` |
|
|
104
|
+
| `app.middleware.ResponseTimeMiddleware.log-base-url` | `false` |
|
|
86
105
|
| `ResponseTimeMiddleware.histogram.buckets` | `[0.005, 0.01, … , 10]` |
|
|
87
106
|
|
|
88
107
|
### `ProxyPrometheusMiddleware`
|
|
@@ -92,13 +111,18 @@ Prometheus‑instrumented HTTP proxy via `http-proxy-middleware`.
|
|
|
92
111
|
```ts
|
|
93
112
|
import { ProxyPrometheusMiddleware } from '@biorate/nestjs-tools';
|
|
94
113
|
|
|
95
|
-
consumer
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
114
|
+
consumer
|
|
115
|
+
.apply(
|
|
116
|
+
ProxyPrometheusMiddleware.create({
|
|
117
|
+
target: 'http://upstream.service:8080',
|
|
118
|
+
changeOrigin: true,
|
|
119
|
+
}),
|
|
120
|
+
)
|
|
121
|
+
.forRoutes('/api/v2/legacy');
|
|
99
122
|
```
|
|
100
123
|
|
|
101
124
|
Creates two metrics:
|
|
125
|
+
|
|
102
126
|
- `http_proxy_server_requests_seconds_count` (counter)
|
|
103
127
|
- `http_proxy_server_requests_seconds` (histogram)
|
|
104
128
|
|
|
@@ -120,7 +144,7 @@ Catches all exceptions and returns a structured JSON response:
|
|
|
120
144
|
}
|
|
121
145
|
```
|
|
122
146
|
|
|
123
|
-
Supports HTTP
|
|
147
|
+
Supports HTTP and WebSocket transports; RPC is unsupported and throws `UnsupportedProtocolError`. Maps `meta.status` from `@biorate/errors` to HTTP status codes.
|
|
124
148
|
|
|
125
149
|
```ts
|
|
126
150
|
app.useGlobalFilters(new AllExceptionsFilter());
|
|
@@ -145,13 +169,38 @@ incoming.response.data
|
|
|
145
169
|
incoming.response.errorCode
|
|
146
170
|
```
|
|
147
171
|
|
|
148
|
-
Config: `TracingInterceptor.excluded` — array of strings or RegExp to skip URLs
|
|
172
|
+
Config: `TracingInterceptor.excluded` — array of strings or RegExp to skip URLs
|
|
173
|
+
(string entries match by substring inclusion, RegExp entries by test).
|
|
174
|
+
|
|
175
|
+
#### Aspect exclusion
|
|
176
|
+
|
|
177
|
+
Granular control over which span attributes are written:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"TracingInterceptor": {
|
|
182
|
+
"excluded": ["/health"],
|
|
183
|
+
"excludedAttributes": ["response.data"],
|
|
184
|
+
"excludedByURL": [{ "url": "/files", "attributes": ["body", "response"] }]
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
- `TracingInterceptor.excludedAttributes` — string[] of aspects to skip globally.
|
|
190
|
+
- `TracingInterceptor.excludedByURL` — array of `{ url: string | RegExp, attributes: string[] }`
|
|
191
|
+
entries; aspects are skipped only for matching URLs (string entries match by
|
|
192
|
+
substring inclusion, RegExp entries by test).
|
|
193
|
+
- An excluded item also matches aspect groups by prefix: `response` excludes
|
|
194
|
+
`response.headers`, `response.statusCode`, `response.data` and `response.errorCode`.
|
|
195
|
+
- Accepted aliases: `url`, `method`, `headers`, `body`, `params`, `query`
|
|
196
|
+
(→ `incoming.request.*`), `statusCode`, `data`, `errorCode` (→ `incoming.response.*`);
|
|
197
|
+
full names (`request.body`, `response.data`, …) work as-is.
|
|
149
198
|
|
|
150
199
|
### `RoutesInterceptor`
|
|
151
200
|
|
|
152
201
|
Resolves the full route path (controller + handler) and stores it in a `WeakMap<Request, { path }>` for use by `RequestCountMiddleware` and `ResponseTimeMiddleware`.
|
|
153
202
|
|
|
154
|
-
|
|
203
|
+
Not applied automatically: when using the count/time middlewares, register it globally via `app.useGlobalInterceptors(new RoutesInterceptor())` — otherwise they won't see the resolved route path and metrics fall back to `/`.
|
|
155
204
|
|
|
156
205
|
## Providers (Guards)
|
|
157
206
|
|
|
@@ -164,7 +213,9 @@ HTTP Basic Authentication guard. Validates `Authorization: Basic …` against co
|
|
|
164
213
|
export class MyController {
|
|
165
214
|
@Get('/admin')
|
|
166
215
|
@AuthBasic()
|
|
167
|
-
public admin() {
|
|
216
|
+
public admin() {
|
|
217
|
+
return 'secret data';
|
|
218
|
+
}
|
|
168
219
|
}
|
|
169
220
|
```
|
|
170
221
|
|
|
@@ -173,13 +224,22 @@ export class MyController {
|
|
|
173
224
|
Bitmask‑based role guard. Use with `@Roles()` decorator:
|
|
174
225
|
|
|
175
226
|
```ts
|
|
227
|
+
import { Controller, Get } from '@nestjs/common';
|
|
176
228
|
import { Roles, UserRoles } from '@biorate/nestjs-tools';
|
|
177
229
|
|
|
178
|
-
@
|
|
179
|
-
|
|
180
|
-
|
|
230
|
+
@Controller()
|
|
231
|
+
export class AdminController {
|
|
232
|
+
@Roles(UserRoles.Admin, UserRoles.Basic)
|
|
233
|
+
@Get('/admin')
|
|
234
|
+
public admin() {
|
|
235
|
+
return 'ok';
|
|
236
|
+
}
|
|
237
|
+
}
|
|
181
238
|
```
|
|
182
239
|
|
|
240
|
+
`Roles(...)` accepts a variadic list of roles (combined into a bitmask via
|
|
241
|
+
bitwise OR internally). Built-in `UserRoles`: `Basic` (`1 << 0`), `Admin` (`1 << 1`).
|
|
242
|
+
|
|
183
243
|
Override `getUser()` to load the user from your database:
|
|
184
244
|
|
|
185
245
|
```ts
|
|
@@ -192,20 +252,12 @@ export class CustomRolesGuard extends RolesGuardProvider {
|
|
|
192
252
|
}
|
|
193
253
|
```
|
|
194
254
|
|
|
195
|
-
### `BootstrapProvider`
|
|
196
|
-
|
|
197
|
-
Convenience middleware that initializes the `@biorate/inversion` container lifecycle:
|
|
198
|
-
|
|
199
|
-
```ts
|
|
200
|
-
app.use(BootstrapProvider.create());
|
|
201
|
-
```
|
|
202
|
-
|
|
203
255
|
## Decorators
|
|
204
256
|
|
|
205
|
-
| Decorator
|
|
206
|
-
|
|
207
|
-
| `@Roles(...)`
|
|
208
|
-
| `@AuthBasic()
|
|
257
|
+
| Decorator | Composition |
|
|
258
|
+
| -------------- | ------------------------------------------------- |
|
|
259
|
+
| `@Roles(...)` | `SetMetadata(ROLES_KEY, bitmask)` |
|
|
260
|
+
| `@AuthBasic()` | `ApiBasicAuth()` + `UseGuards(AuthGuardProvider)` |
|
|
209
261
|
|
|
210
262
|
## CORS
|
|
211
263
|
|
|
@@ -219,15 +271,18 @@ In non‑debug environments, validates the origin against `ALLOWED_ORIGIN` confi
|
|
|
219
271
|
|
|
220
272
|
## HTTP controllers
|
|
221
273
|
|
|
274
|
+
> The `MetricsController`, `ProbeController`, and `LocalesController` in `src/http/controllers` are marked `@deprecated`. Their hexagonal replacements live in `src/app/infrastructure/controllers/` and are **not** exported individually at the package top level — `src/app/infrastructure/index.ts` re-exports them only as the `controllers` namespace (`export * as controllers from './controllers'`), so reach them as `import { controllers } from '@biorate/nestjs-tools'` (e.g. `controllers.MetricsController`).
|
|
275
|
+
|
|
222
276
|
### `MetricsController`
|
|
223
277
|
|
|
224
278
|
Exposes `GET /metrics` with Prometheus metrics from the shared registry.
|
|
225
279
|
|
|
226
280
|
### `ProbeController`
|
|
227
281
|
|
|
228
|
-
| Route
|
|
229
|
-
|
|
230
|
-
| `GET /probe`
|
|
282
|
+
| Route | Purpose |
|
|
283
|
+
| ---------------------- | --------------- |
|
|
284
|
+
| `GET /probe/readiness` | Readiness probe |
|
|
285
|
+
| `GET /probe/healthz` | Healthz probe |
|
|
231
286
|
|
|
232
287
|
### `LocalesController`
|
|
233
288
|
|
|
@@ -255,14 +310,12 @@ src/app/
|
|
|
255
310
|
└──────────┬───────────────────────────────────────────┘
|
|
256
311
|
│
|
|
257
312
|
┌──────────▼───────────────────────────────────────────┐
|
|
258
|
-
│
|
|
313
|
+
│ RoutesInterceptor (path resolution) │
|
|
259
314
|
├──────────────────────────────────────────────────────┤
|
|
260
315
|
│ RequestCountMiddleware (counter) │
|
|
261
316
|
├──────────────────────────────────────────────────────┤
|
|
262
317
|
│ ResponseTimeMiddleware (histogram + header) │
|
|
263
318
|
├──────────────────────────────────────────────────────┤
|
|
264
|
-
│ RoutesInterceptor (path resolution) │
|
|
265
|
-
├──────────────────────────────────────────────────────┤
|
|
266
319
|
│ TracingInterceptor (OpenTelemetry span attrs) │
|
|
267
320
|
├──────────────────────────────────────────────────────┤
|
|
268
321
|
│ AllExceptionsFilter (structured error JSON) │
|
|
@@ -11,6 +11,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
11
11
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
|
+
var TracingInterceptor_1;
|
|
14
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
16
|
exports.TracingInterceptor = void 0;
|
|
16
17
|
const json_stringify_safe_1 = __importDefault(require("json-stringify-safe"));
|
|
@@ -18,44 +19,78 @@ const rxjs_1 = require("rxjs");
|
|
|
18
19
|
const opentelemetry_1 = require("@biorate/opentelemetry");
|
|
19
20
|
const common_1 = require("@nestjs/common");
|
|
20
21
|
const inversion_1 = require("@biorate/inversion");
|
|
21
|
-
|
|
22
|
+
const lodash_es_1 = require("lodash-es");
|
|
23
|
+
let TracingInterceptor = TracingInterceptor_1 = class TracingInterceptor {
|
|
24
|
+
static normalizeAspect(name) {
|
|
25
|
+
const aliased = (0, lodash_es_1.get)(TracingInterceptor_1.aspectAliases, name);
|
|
26
|
+
if (aliased)
|
|
27
|
+
return aliased;
|
|
28
|
+
if (name.startsWith('request.'))
|
|
29
|
+
return name.slice('request.'.length);
|
|
30
|
+
return name;
|
|
31
|
+
}
|
|
22
32
|
stringify(data) {
|
|
23
33
|
return typeof data === 'object' ? (0, json_stringify_safe_1.default)(data) : String(data);
|
|
24
34
|
}
|
|
25
35
|
excluded(url) {
|
|
26
36
|
const excluded = this.config.get('TracingInterceptor.excluded', []);
|
|
27
37
|
for (const item of excluded) {
|
|
28
|
-
if (typeof item === 'string' && url.
|
|
38
|
+
if (typeof item === 'string' && url.includes(item))
|
|
29
39
|
return true;
|
|
30
40
|
if (item instanceof RegExp && item.test(url))
|
|
31
41
|
return true;
|
|
32
42
|
}
|
|
33
43
|
return false;
|
|
34
44
|
}
|
|
45
|
+
excludedAttribute(url, aspect) {
|
|
46
|
+
const canonical = TracingInterceptor_1.normalizeAspect(aspect);
|
|
47
|
+
const match = (list) => list.some((item) => {
|
|
48
|
+
const target = TracingInterceptor_1.normalizeAspect(item);
|
|
49
|
+
return canonical === target || canonical.startsWith(`${target}.`);
|
|
50
|
+
});
|
|
51
|
+
if (match(this.config.get('TracingInterceptor.excludedAttributes', [])))
|
|
52
|
+
return true;
|
|
53
|
+
const byUrl = this.config.get('TracingInterceptor.excludedByURL', []);
|
|
54
|
+
for (const entry of byUrl) {
|
|
55
|
+
const hit = typeof entry?.url === 'string'
|
|
56
|
+
? url.includes(entry.url)
|
|
57
|
+
: entry?.url instanceof RegExp && entry.url.test(url);
|
|
58
|
+
if (!hit)
|
|
59
|
+
continue;
|
|
60
|
+
if (match(entry?.attributes ?? []))
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
attr(span, url, aspect, value) {
|
|
66
|
+
if (this.excludedAttribute(url, aspect))
|
|
67
|
+
return;
|
|
68
|
+
span.setAttribute(TracingInterceptor_1.aspectKeys[aspect], this.stringify(value));
|
|
69
|
+
}
|
|
35
70
|
http(span, context, next) {
|
|
36
71
|
const host = context.switchToHttp();
|
|
37
72
|
const req = host.getRequest();
|
|
38
73
|
const res = host.getResponse();
|
|
39
74
|
if (this.excluded(req.url))
|
|
40
75
|
return next.handle();
|
|
41
|
-
|
|
42
|
-
span.
|
|
43
|
-
span.
|
|
44
|
-
span.
|
|
45
|
-
span.
|
|
46
|
-
span.
|
|
76
|
+
this.attr(span, req.url, 'url', req.url);
|
|
77
|
+
this.attr(span, req.url, 'body', req.body);
|
|
78
|
+
this.attr(span, req.url, 'headers', req.headers);
|
|
79
|
+
this.attr(span, req.url, 'method', req.method);
|
|
80
|
+
this.attr(span, req.url, 'params', req.params);
|
|
81
|
+
this.attr(span, req.url, 'query', req.query);
|
|
47
82
|
return next.handle().pipe((0, rxjs_1.catchError)((e) => {
|
|
48
|
-
span.
|
|
49
|
-
span.
|
|
50
|
-
span.
|
|
51
|
-
span.
|
|
83
|
+
this.attr(span, req.url, 'response.headers', res.headers);
|
|
84
|
+
this.attr(span, req.url, 'response.statusCode', res.statusCode);
|
|
85
|
+
this.attr(span, req.url, 'response.errorCode', e.code);
|
|
86
|
+
this.attr(span, req.url, 'response.data', e?.response?.data);
|
|
52
87
|
span.recordException(e);
|
|
53
88
|
span.setStatus({ code: opentelemetry_1.SpanStatusCode.ERROR });
|
|
54
89
|
throw e;
|
|
55
90
|
}), (0, rxjs_1.tap)((data) => {
|
|
56
|
-
span.
|
|
57
|
-
span.
|
|
58
|
-
span.
|
|
91
|
+
this.attr(span, req.url, 'response.headers', res.headers);
|
|
92
|
+
this.attr(span, req.url, 'response.statusCode', res.statusCode);
|
|
93
|
+
this.attr(span, req.url, 'response.data', data);
|
|
59
94
|
}));
|
|
60
95
|
}
|
|
61
96
|
ws(span, context, next) {
|
|
@@ -73,10 +108,33 @@ let TracingInterceptor = class TracingInterceptor {
|
|
|
73
108
|
}
|
|
74
109
|
};
|
|
75
110
|
exports.TracingInterceptor = TracingInterceptor;
|
|
111
|
+
TracingInterceptor.aspectAliases = {
|
|
112
|
+
'request.url': 'url',
|
|
113
|
+
'request.method': 'method',
|
|
114
|
+
'request.headers': 'headers',
|
|
115
|
+
'request.body': 'body',
|
|
116
|
+
'request.params': 'params',
|
|
117
|
+
'request.query': 'query',
|
|
118
|
+
statusCode: 'response.statusCode',
|
|
119
|
+
data: 'response.data',
|
|
120
|
+
errorCode: 'response.errorCode',
|
|
121
|
+
};
|
|
122
|
+
TracingInterceptor.aspectKeys = {
|
|
123
|
+
url: 'incoming.request.url',
|
|
124
|
+
method: 'incoming.request.method',
|
|
125
|
+
headers: 'incoming.request.headers',
|
|
126
|
+
body: 'incoming.request.body',
|
|
127
|
+
params: 'incoming.request.params',
|
|
128
|
+
query: 'incoming.request.query',
|
|
129
|
+
'response.headers': 'incoming.response.headers',
|
|
130
|
+
'response.statusCode': 'incoming.response.statusCode',
|
|
131
|
+
'response.data': 'incoming.response.data',
|
|
132
|
+
'response.errorCode': 'incoming.response.errorCode',
|
|
133
|
+
};
|
|
76
134
|
__decorate([
|
|
77
135
|
(0, inversion_1.inject)(inversion_1.Types.Config),
|
|
78
136
|
__metadata("design:type", Object)
|
|
79
137
|
], TracingInterceptor.prototype, "config", void 0);
|
|
80
|
-
exports.TracingInterceptor = TracingInterceptor = __decorate([
|
|
138
|
+
exports.TracingInterceptor = TracingInterceptor = TracingInterceptor_1 = __decorate([
|
|
81
139
|
(0, common_1.Injectable)()
|
|
82
140
|
], TracingInterceptor);
|
|
@@ -7,49 +7,84 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
8
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
9
|
};
|
|
10
|
+
var TracingInterceptor_1;
|
|
10
11
|
import stringify from 'json-stringify-safe';
|
|
11
12
|
import { tap, catchError } from 'rxjs';
|
|
12
13
|
import { trace, SpanStatusCode } from '@biorate/opentelemetry';
|
|
13
14
|
import { Injectable, } from '@nestjs/common';
|
|
14
15
|
import { inject, Types } from '@biorate/inversion';
|
|
15
|
-
|
|
16
|
+
import { get } from 'lodash-es';
|
|
17
|
+
let TracingInterceptor = TracingInterceptor_1 = class TracingInterceptor {
|
|
18
|
+
static normalizeAspect(name) {
|
|
19
|
+
const aliased = get(TracingInterceptor_1.aspectAliases, name);
|
|
20
|
+
if (aliased)
|
|
21
|
+
return aliased;
|
|
22
|
+
if (name.startsWith('request.'))
|
|
23
|
+
return name.slice('request.'.length);
|
|
24
|
+
return name;
|
|
25
|
+
}
|
|
16
26
|
stringify(data) {
|
|
17
27
|
return typeof data === 'object' ? stringify(data) : String(data);
|
|
18
28
|
}
|
|
19
29
|
excluded(url) {
|
|
20
30
|
const excluded = this.config.get('TracingInterceptor.excluded', []);
|
|
21
31
|
for (const item of excluded) {
|
|
22
|
-
if (typeof item === 'string' && url.
|
|
32
|
+
if (typeof item === 'string' && url.includes(item))
|
|
23
33
|
return true;
|
|
24
34
|
if (item instanceof RegExp && item.test(url))
|
|
25
35
|
return true;
|
|
26
36
|
}
|
|
27
37
|
return false;
|
|
28
38
|
}
|
|
39
|
+
excludedAttribute(url, aspect) {
|
|
40
|
+
const canonical = TracingInterceptor_1.normalizeAspect(aspect);
|
|
41
|
+
const match = (list) => list.some((item) => {
|
|
42
|
+
const target = TracingInterceptor_1.normalizeAspect(item);
|
|
43
|
+
return canonical === target || canonical.startsWith(`${target}.`);
|
|
44
|
+
});
|
|
45
|
+
if (match(this.config.get('TracingInterceptor.excludedAttributes', [])))
|
|
46
|
+
return true;
|
|
47
|
+
const byUrl = this.config.get('TracingInterceptor.excludedByURL', []);
|
|
48
|
+
for (const entry of byUrl) {
|
|
49
|
+
const hit = typeof entry?.url === 'string'
|
|
50
|
+
? url.includes(entry.url)
|
|
51
|
+
: entry?.url instanceof RegExp && entry.url.test(url);
|
|
52
|
+
if (!hit)
|
|
53
|
+
continue;
|
|
54
|
+
if (match(entry?.attributes ?? []))
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
attr(span, url, aspect, value) {
|
|
60
|
+
if (this.excludedAttribute(url, aspect))
|
|
61
|
+
return;
|
|
62
|
+
span.setAttribute(TracingInterceptor_1.aspectKeys[aspect], this.stringify(value));
|
|
63
|
+
}
|
|
29
64
|
http(span, context, next) {
|
|
30
65
|
const host = context.switchToHttp();
|
|
31
66
|
const req = host.getRequest();
|
|
32
67
|
const res = host.getResponse();
|
|
33
68
|
if (this.excluded(req.url))
|
|
34
69
|
return next.handle();
|
|
35
|
-
|
|
36
|
-
span.
|
|
37
|
-
span.
|
|
38
|
-
span.
|
|
39
|
-
span.
|
|
40
|
-
span.
|
|
70
|
+
this.attr(span, req.url, 'url', req.url);
|
|
71
|
+
this.attr(span, req.url, 'body', req.body);
|
|
72
|
+
this.attr(span, req.url, 'headers', req.headers);
|
|
73
|
+
this.attr(span, req.url, 'method', req.method);
|
|
74
|
+
this.attr(span, req.url, 'params', req.params);
|
|
75
|
+
this.attr(span, req.url, 'query', req.query);
|
|
41
76
|
return next.handle().pipe(catchError((e) => {
|
|
42
|
-
span.
|
|
43
|
-
span.
|
|
44
|
-
span.
|
|
45
|
-
span.
|
|
77
|
+
this.attr(span, req.url, 'response.headers', res.headers);
|
|
78
|
+
this.attr(span, req.url, 'response.statusCode', res.statusCode);
|
|
79
|
+
this.attr(span, req.url, 'response.errorCode', e.code);
|
|
80
|
+
this.attr(span, req.url, 'response.data', e?.response?.data);
|
|
46
81
|
span.recordException(e);
|
|
47
82
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
48
83
|
throw e;
|
|
49
84
|
}), tap((data) => {
|
|
50
|
-
span.
|
|
51
|
-
span.
|
|
52
|
-
span.
|
|
85
|
+
this.attr(span, req.url, 'response.headers', res.headers);
|
|
86
|
+
this.attr(span, req.url, 'response.statusCode', res.statusCode);
|
|
87
|
+
this.attr(span, req.url, 'response.data', data);
|
|
53
88
|
}));
|
|
54
89
|
}
|
|
55
90
|
ws(span, context, next) {
|
|
@@ -66,11 +101,34 @@ let TracingInterceptor = class TracingInterceptor {
|
|
|
66
101
|
return this[type](span, context, next);
|
|
67
102
|
}
|
|
68
103
|
};
|
|
104
|
+
TracingInterceptor.aspectAliases = {
|
|
105
|
+
'request.url': 'url',
|
|
106
|
+
'request.method': 'method',
|
|
107
|
+
'request.headers': 'headers',
|
|
108
|
+
'request.body': 'body',
|
|
109
|
+
'request.params': 'params',
|
|
110
|
+
'request.query': 'query',
|
|
111
|
+
statusCode: 'response.statusCode',
|
|
112
|
+
data: 'response.data',
|
|
113
|
+
errorCode: 'response.errorCode',
|
|
114
|
+
};
|
|
115
|
+
TracingInterceptor.aspectKeys = {
|
|
116
|
+
url: 'incoming.request.url',
|
|
117
|
+
method: 'incoming.request.method',
|
|
118
|
+
headers: 'incoming.request.headers',
|
|
119
|
+
body: 'incoming.request.body',
|
|
120
|
+
params: 'incoming.request.params',
|
|
121
|
+
query: 'incoming.request.query',
|
|
122
|
+
'response.headers': 'incoming.response.headers',
|
|
123
|
+
'response.statusCode': 'incoming.response.statusCode',
|
|
124
|
+
'response.data': 'incoming.response.data',
|
|
125
|
+
'response.errorCode': 'incoming.response.errorCode',
|
|
126
|
+
};
|
|
69
127
|
__decorate([
|
|
70
128
|
inject(Types.Config),
|
|
71
129
|
__metadata("design:type", Object)
|
|
72
130
|
], TracingInterceptor.prototype, "config", void 0);
|
|
73
|
-
TracingInterceptor = __decorate([
|
|
131
|
+
TracingInterceptor = TracingInterceptor_1 = __decorate([
|
|
74
132
|
Injectable()
|
|
75
133
|
], TracingInterceptor);
|
|
76
134
|
export { TracingInterceptor };
|
|
@@ -2,9 +2,14 @@ import { Span } from '@biorate/opentelemetry';
|
|
|
2
2
|
import { NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
|
|
3
3
|
import { IConfig } from '@biorate/config';
|
|
4
4
|
export declare class TracingInterceptor implements NestInterceptor {
|
|
5
|
+
protected static aspectAliases: Record<string, string>;
|
|
6
|
+
protected static aspectKeys: Record<string, string>;
|
|
7
|
+
protected static normalizeAspect(name: string): string;
|
|
5
8
|
protected readonly config: IConfig;
|
|
6
9
|
protected stringify(data: unknown): string;
|
|
7
10
|
protected excluded(url: string): boolean;
|
|
11
|
+
protected excludedAttribute(url: string, aspect: string): boolean;
|
|
12
|
+
protected attr(span: Span, url: string, aspect: string, value: unknown): void;
|
|
8
13
|
protected http(span: Span, context: ExecutionContext, next: CallHandler): import("rxjs").Observable<any>;
|
|
9
14
|
protected ws(span: Span, context: ExecutionContext, next: CallHandler): import("rxjs").Observable<any>;
|
|
10
15
|
protected rpc(span: Span, context: ExecutionContext, next: CallHandler): import("rxjs").Observable<any>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@biorate/nestjs-tools",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.2",
|
|
4
4
|
"description": "NestJS tools",
|
|
5
5
|
"main": "./dist/cjs/index.js",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"keywords": [],
|
|
46
46
|
"author": "llevkin",
|
|
47
47
|
"license": "MIT",
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "53c9b4fbcae2fd034c3ba129a5cbe52f4b80bff3",
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@nestjs/common": ">=10.4.15",
|
|
51
51
|
"@nestjs/core": ">=10.4.15",
|
|
@@ -59,13 +59,13 @@
|
|
|
59
59
|
"ws": ">=8.18.0"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@biorate/axios": "3.4.
|
|
63
|
-
"@biorate/config": "3.2.
|
|
62
|
+
"@biorate/axios": "3.4.1",
|
|
63
|
+
"@biorate/config": "3.2.4",
|
|
64
64
|
"@biorate/errors": "3.1.2",
|
|
65
|
-
"@biorate/inversion": "3.1.
|
|
66
|
-
"@biorate/opentelemetry": "2.4.
|
|
67
|
-
"@biorate/prometheus": "3.1.
|
|
68
|
-
"@biorate/tools": "3.1.
|
|
65
|
+
"@biorate/inversion": "3.1.4",
|
|
66
|
+
"@biorate/opentelemetry": "2.4.3",
|
|
67
|
+
"@biorate/prometheus": "3.1.4",
|
|
68
|
+
"@biorate/tools": "3.1.3",
|
|
69
69
|
"http-proxy": "1.18.1",
|
|
70
70
|
"http-proxy-middleware": "2.0.6",
|
|
71
71
|
"json-stringify-safe": "5.0.1",
|