@biorate/nestjs-tools 2.4.0 → 2.5.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/README.md CHANGED
@@ -77,12 +77,12 @@ app.use(ResponseTimeMiddleware.create());
77
77
 
78
78
  Config options:
79
79
 
80
- | Path | Default |
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` |
80
+ | Path | Default |
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` |
86
86
  | `ResponseTimeMiddleware.histogram.buckets` | `[0.005, 0.01, … , 10]` |
87
87
 
88
88
  ### `ProxyPrometheusMiddleware`
@@ -92,13 +92,18 @@ Prometheus‑instrumented HTTP proxy via `http-proxy-middleware`.
92
92
  ```ts
93
93
  import { ProxyPrometheusMiddleware } from '@biorate/nestjs-tools';
94
94
 
95
- consumer.apply(ProxyPrometheusMiddleware.create({
96
- target: 'http://upstream.service:8080',
97
- changeOrigin: true,
98
- })).forRoutes('/api/v2/legacy');
95
+ consumer
96
+ .apply(
97
+ ProxyPrometheusMiddleware.create({
98
+ target: 'http://upstream.service:8080',
99
+ changeOrigin: true,
100
+ }),
101
+ )
102
+ .forRoutes('/api/v2/legacy');
99
103
  ```
100
104
 
101
105
  Creates two metrics:
106
+
102
107
  - `http_proxy_server_requests_seconds_count` (counter)
103
108
  - `http_proxy_server_requests_seconds` (histogram)
104
109
 
@@ -145,7 +150,32 @@ incoming.response.data
145
150
  incoming.response.errorCode
146
151
  ```
147
152
 
148
- Config: `TracingInterceptor.excluded` — array of strings or RegExp to skip URLs.
153
+ Config: `TracingInterceptor.excluded` — array of strings or RegExp to skip URLs
154
+ (string entries match by substring inclusion, RegExp entries by test).
155
+
156
+ #### Aspect exclusion
157
+
158
+ Granular control over which span attributes are written:
159
+
160
+ ```json
161
+ {
162
+ "TracingInterceptor": {
163
+ "excluded": ["/health"],
164
+ "excludedAttributes": ["response.data"],
165
+ "excludedByURL": [{ "url": "/files", "attributes": ["body", "response"] }]
166
+ }
167
+ }
168
+ ```
169
+
170
+ - `TracingInterceptor.excludedAttributes` — string[] of aspects to skip globally.
171
+ - `TracingInterceptor.excludedByURL` — array of `{ url: string | RegExp, attributes: string[] }`
172
+ entries; aspects are skipped only for matching URLs (string entries match by
173
+ substring inclusion, RegExp entries by test).
174
+ - An excluded item also matches aspect groups by prefix: `response` excludes
175
+ `response.headers`, `response.statusCode`, `response.data` and `response.errorCode`.
176
+ - Accepted aliases: `url`, `method`, `headers`, `body`, `params`, `query`
177
+ (→ `incoming.request.*`), `statusCode`, `data`, `errorCode` (→ `incoming.response.*`);
178
+ full names (`request.body`, `response.data`, …) work as-is.
149
179
 
150
180
  ### `RoutesInterceptor`
151
181
 
@@ -164,7 +194,9 @@ HTTP Basic Authentication guard. Validates `Authorization: Basic …` against co
164
194
  export class MyController {
165
195
  @Get('/admin')
166
196
  @AuthBasic()
167
- public admin() { return 'secret data'; }
197
+ public admin() {
198
+ return 'secret data';
199
+ }
168
200
  }
169
201
  ```
170
202
 
@@ -173,13 +205,22 @@ export class MyController {
173
205
  Bitmask‑based role guard. Use with `@Roles()` decorator:
174
206
 
175
207
  ```ts
208
+ import { Controller, Get } from '@nestjs/common';
176
209
  import { Roles, UserRoles } from '@biorate/nestjs-tools';
177
210
 
178
- @Roles(UserRoles.Admin | UserRoles.Manager)
179
- @Get('/admin')
180
- public admin() { return 'ok'; }
211
+ @Controller()
212
+ export class AdminController {
213
+ @Roles(UserRoles.Admin, UserRoles.Basic)
214
+ @Get('/admin')
215
+ public admin() {
216
+ return 'ok';
217
+ }
218
+ }
181
219
  ```
182
220
 
221
+ `Roles(...)` accepts a variadic list of roles (combined into a bitmask via
222
+ bitwise OR internally). Built-in `UserRoles`: `Basic` (`1 << 0`), `Admin` (`1 << 1`).
223
+
183
224
  Override `getUser()` to load the user from your database:
184
225
 
185
226
  ```ts
@@ -202,10 +243,10 @@ app.use(BootstrapProvider.create());
202
243
 
203
244
  ## Decorators
204
245
 
205
- | Decorator | Composition |
206
- |---------------|--------------------------------------------------|
207
- | `@Roles(...)` | `SetMetadata(ROLES_KEY, bitmask)` |
208
- | `@AuthBasic()`| `ApiBasicAuth()` + `UseGuards(AuthGuardProvider)` |
246
+ | Decorator | Composition |
247
+ | -------------- | ------------------------------------------------- |
248
+ | `@Roles(...)` | `SetMetadata(ROLES_KEY, bitmask)` |
249
+ | `@AuthBasic()` | `ApiBasicAuth()` + `UseGuards(AuthGuardProvider)` |
209
250
 
210
251
  ## CORS
211
252
 
@@ -225,9 +266,9 @@ Exposes `GET /metrics` with Prometheus metrics from the shared registry.
225
266
 
226
267
  ### `ProbeController`
227
268
 
228
- | Route | Purpose |
229
- |-----------------|---------------------|
230
- | `GET /probe` | Liveness/readiness |
269
+ | Route | Purpose |
270
+ | ------------ | ------------------ |
271
+ | `GET /probe` | Liveness/readiness |
231
272
 
232
273
  ### `LocalesController`
233
274
 
@@ -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
- let TracingInterceptor = class TracingInterceptor {
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.startsWith(item))
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
- span.setAttribute('incoming.request.url', this.stringify(req.url));
42
- span.setAttribute('incoming.request.body', this.stringify(req.body));
43
- span.setAttribute('incoming.request.headers', this.stringify(req.headers));
44
- span.setAttribute('incoming.request.method', this.stringify(req.method));
45
- span.setAttribute('incoming.request.params', this.stringify(req.params));
46
- span.setAttribute('incoming.request.query', this.stringify(req.query));
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.setAttribute('incoming.response.headers', this.stringify(res.headers));
49
- span.setAttribute('incoming.response.statusCode', this.stringify(res.statusCode));
50
- span.setAttribute('incoming.response.errorCode', this.stringify(e.code));
51
- span.setAttribute('incoming.response.data', this.stringify(e?.response?.data));
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.setAttribute('incoming.response.headers', this.stringify(res.headers));
57
- span.setAttribute('incoming.response.statusCode', this.stringify(res.statusCode));
58
- span.setAttribute('incoming.response.data', this.stringify(data));
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
- let TracingInterceptor = class TracingInterceptor {
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.startsWith(item))
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
- span.setAttribute('incoming.request.url', this.stringify(req.url));
36
- span.setAttribute('incoming.request.body', this.stringify(req.body));
37
- span.setAttribute('incoming.request.headers', this.stringify(req.headers));
38
- span.setAttribute('incoming.request.method', this.stringify(req.method));
39
- span.setAttribute('incoming.request.params', this.stringify(req.params));
40
- span.setAttribute('incoming.request.query', this.stringify(req.query));
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.setAttribute('incoming.response.headers', this.stringify(res.headers));
43
- span.setAttribute('incoming.response.statusCode', this.stringify(res.statusCode));
44
- span.setAttribute('incoming.response.errorCode', this.stringify(e.code));
45
- span.setAttribute('incoming.response.data', this.stringify(e?.response?.data));
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.setAttribute('incoming.response.headers', this.stringify(res.headers));
51
- span.setAttribute('incoming.response.statusCode', this.stringify(res.statusCode));
52
- span.setAttribute('incoming.response.data', this.stringify(data));
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.4.0",
3
+ "version": "2.5.0",
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": "09ccd82d8fba90cb37750dccf41469452880b129",
48
+ "gitHead": "8c6857bfe61de7b03025b0d883c478045c14390a",
49
49
  "peerDependencies": {
50
50
  "@nestjs/common": ">=10.4.15",
51
51
  "@nestjs/core": ">=10.4.15",