@application-kit/observability 0.10.0 → 0.12.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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @application-kit/observability
2
2
 
3
+ ## 0.12.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [df7cdd0]
8
+ - @application-kit/typeorm@0.12.0
9
+
10
+ ## 0.11.0
11
+
12
+ ### Minor Changes
13
+
14
+ - 523d0e9: Строка лога о запросе больше не содержит секретов: `set-cookie` ответа заменяется маркером, значения параметров `token` и `code` вырезаются из `req.url`, `req.query` и заголовка `referer`, в том числе внутри другого параметра. Новые опции логгера `redactPaths` и `sensitiveQueryParameters` дополняют дефолт пакета.
15
+
16
+ ### Patch Changes
17
+
18
+ - 8e10f98: Peer-зависимости принимают Nest 12, `@nestjs/graphql` и `@nestjs/mercurius` 14, `@nestjs/cqrs` 12 и `nestjs-pino` 5. `PinoLoggerConfig` задаёт `forRoutes` явно: с маршрутом nestjs-pino 5 по умолчанию запросы на Fastify не логировались.
19
+ - Updated dependencies [8e10f98]
20
+ - @application-kit/typeorm@0.5.3
21
+
3
22
  ## 0.10.0
4
23
 
5
24
  ### Minor Changes
@@ -5,5 +5,6 @@ export * from './logger.environment.js';
5
5
  export * from './observability-logger.interface.js';
6
6
  export * from './pino-logger-options.interface.js';
7
7
  export * from './pino-logger.config.js';
8
+ export * from './redact-query-parameters.js';
8
9
  export * from './resolve-request-id.js';
9
10
  export * from './resolve-support-id.js';
@@ -21,5 +21,6 @@ __exportStar(require("./logger.environment.js"), exports);
21
21
  __exportStar(require("./observability-logger.interface.js"), exports);
22
22
  __exportStar(require("./pino-logger-options.interface.js"), exports);
23
23
  __exportStar(require("./pino-logger.config.js"), exports);
24
+ __exportStar(require("./redact-query-parameters.js"), exports);
24
25
  __exportStar(require("./resolve-request-id.js"), exports);
25
26
  __exportStar(require("./resolve-support-id.js"), exports);
@@ -1,5 +1,7 @@
1
1
  import type { ClientIpSourceInterface } from '../http/resolve-client-ip.js';
2
2
  export type ClientIpResolver = (request: ClientIpSourceInterface) => string;
3
3
  export interface PinoLoggerOptionsInterface {
4
+ readonly redactPaths?: readonly string[];
4
5
  readonly resolveClientIp?: ClientIpResolver;
6
+ readonly sensitiveQueryParameters?: readonly string[];
5
7
  }
@@ -18,6 +18,7 @@ const resolve_client_ip_js_1 = require("../http/resolve-client-ip.js");
18
18
  const active_span_context_js_1 = require("../shared/active-span-context.js");
19
19
  const infrastructure_paths_js_1 = require("./infrastructure-paths.js");
20
20
  const logger_environment_js_1 = require("./logger.environment.js");
21
+ const redact_query_parameters_js_1 = require("./redact-query-parameters.js");
21
22
  const HTTP_STATUS_CLIENT_ERROR = 400;
22
23
  const HTTP_STATUS_SERVER_ERROR = 500;
23
24
  const isInfrastructureRequest = (url) => {
@@ -55,10 +56,19 @@ const REDACTED_LOG_PATHS = [
55
56
  'password',
56
57
  'req.headers.authorization',
57
58
  'req.headers.cookie',
59
+ 'res.headers["set-cookie"]',
58
60
  ];
59
61
  const rawSocketPort = (request) => request.raw?.socket?.remotePort;
60
62
  const remotePort = (request) => request.socket?.remotePort
61
63
  ?? rawSocketPort(request);
64
+ const withRedactedReferer = (headers, sensitiveNames) => (headers.referer === undefined
65
+ ? headers
66
+ : { ...headers, referer: (0, redact_query_parameters_js_1.redactUrlParameters)(headers.referer, sensitiveNames) });
67
+ const sensitiveNamesOf = ({ sensitiveQueryParameters = [] }) => new Set([...redact_query_parameters_js_1.DEFAULT_SENSITIVE_QUERY_PARAMETERS, ...sensitiveQueryParameters]);
68
+ const redactPathsOf = ({ redactPaths = [] }) => [
69
+ ...REDACTED_LOG_PATHS,
70
+ ...redactPaths,
71
+ ];
62
72
  let PinoLoggerConfig = class PinoLoggerConfig {
63
73
  environment;
64
74
  constructor(environment) {
@@ -66,7 +76,9 @@ let PinoLoggerConfig = class PinoLoggerConfig {
66
76
  }
67
77
  createOptions(options = {}) {
68
78
  const clientIp = options.resolveClientIp ?? resolve_client_ip_js_1.resolveClientIp;
79
+ const sensitiveNames = sensitiveNamesOf(options);
69
80
  return {
81
+ forRoutes: [{ method: common_1.RequestMethod.ALL, path: '/{*splat}' }],
70
82
  pinoHttp: {
71
83
  autoLogging: true,
72
84
  customLogLevel: exports.responseLogLevel,
@@ -79,17 +91,17 @@ let PinoLoggerConfig = class PinoLoggerConfig {
79
91
  level: this.environment.LOG_LEVEL,
80
92
  mixin: exports.traceContextMixin,
81
93
  quietReqLogger: true,
82
- redact: [...REDACTED_LOG_PATHS],
94
+ redact: redactPathsOf(options),
83
95
  serializers: {
84
96
  req: (request) => ({
85
- headers: request.headers,
97
+ headers: withRedactedReferer(request.headers, sensitiveNames),
86
98
  id: request.id,
87
99
  method: request.method,
88
100
  params: request.raw?.params,
89
- query: request.query,
101
+ query: (0, redact_query_parameters_js_1.redactQueryObject)(request.query, sensitiveNames),
90
102
  remoteAddress: clientIp(request),
91
103
  remotePort: remotePort(request),
92
- url: request.url,
104
+ url: (0, redact_query_parameters_js_1.redactUrlParameters)(request.url, sensitiveNames),
93
105
  }),
94
106
  },
95
107
  },
@@ -0,0 +1,3 @@
1
+ export declare const DEFAULT_SENSITIVE_QUERY_PARAMETERS: readonly string[];
2
+ export declare const redactUrlParameters: (value: unknown, sensitiveNames: ReadonlySet<string>) => unknown;
3
+ export declare const redactQueryObject: (query: unknown, sensitiveNames: ReadonlySet<string>) => unknown;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.redactQueryObject = exports.redactUrlParameters = exports.DEFAULT_SENSITIVE_QUERY_PARAMETERS = void 0;
4
+ exports.DEFAULT_SENSITIVE_QUERY_PARAMETERS = ['code', 'token'];
5
+ const REDACTED_VALUE = '[Redacted]';
6
+ const PARAMETER_DELIMITER = /(?<delimiter>[&?])/u;
7
+ const redactParameter = (parameter, sensitiveNames) => {
8
+ const assignmentIndex = parameter.indexOf('=');
9
+ return parameter.includes('=') && sensitiveNames.has(parameter.slice(0, assignmentIndex))
10
+ ? `${parameter.slice(0, assignmentIndex + 1)}${REDACTED_VALUE}`
11
+ : parameter;
12
+ };
13
+ const redactUrl = (url, sensitiveNames) => {
14
+ const [path, ...rest] = url.split(PARAMETER_DELIMITER);
15
+ return [path, ...rest.map((part) => redactParameter(part, sensitiveNames))].join('');
16
+ };
17
+ const redactUrlParameters = (value, sensitiveNames) => {
18
+ if (Array.isArray(value)) {
19
+ return value.map((item) => (0, exports.redactUrlParameters)(item, sensitiveNames));
20
+ }
21
+ return typeof value === 'string' ? redactUrl(value, sensitiveNames) : value;
22
+ };
23
+ exports.redactUrlParameters = redactUrlParameters;
24
+ const redactQueryObject = (query, sensitiveNames) => {
25
+ if (typeof query !== 'object' || query === null) {
26
+ return query;
27
+ }
28
+ return Object.fromEntries(Object.entries(query).map(([name, value]) => [
29
+ name,
30
+ sensitiveNames.has(name) ? REDACTED_VALUE : (0, exports.redactUrlParameters)(value, sensitiveNames),
31
+ ]));
32
+ };
33
+ exports.redactQueryObject = redactQueryObject;
@@ -5,5 +5,6 @@ export * from './logger.environment.js';
5
5
  export * from './observability-logger.interface.js';
6
6
  export * from './pino-logger-options.interface.js';
7
7
  export * from './pino-logger.config.js';
8
+ export * from './redact-query-parameters.js';
8
9
  export * from './resolve-request-id.js';
9
10
  export * from './resolve-support-id.js';
@@ -5,5 +5,6 @@ export * from './logger.environment.js';
5
5
  export * from './observability-logger.interface.js';
6
6
  export * from './pino-logger-options.interface.js';
7
7
  export * from './pino-logger.config.js';
8
+ export * from './redact-query-parameters.js';
8
9
  export * from './resolve-request-id.js';
9
10
  export * from './resolve-support-id.js';
@@ -1,5 +1,7 @@
1
1
  import type { ClientIpSourceInterface } from '../http/resolve-client-ip.js';
2
2
  export type ClientIpResolver = (request: ClientIpSourceInterface) => string;
3
3
  export interface PinoLoggerOptionsInterface {
4
+ readonly redactPaths?: readonly string[];
4
5
  readonly resolveClientIp?: ClientIpResolver;
6
+ readonly sensitiveQueryParameters?: readonly string[];
5
7
  }
@@ -10,11 +10,12 @@ var __metadata = (this && this.__metadata) || function (k, v) {
10
10
  var __param = (this && this.__param) || function (paramIndex, decorator) {
11
11
  return function (target, key) { decorator(target, key, paramIndex); }
12
12
  };
13
- import { Inject, Injectable } from '@nestjs/common';
13
+ import { Inject, Injectable, RequestMethod } from '@nestjs/common';
14
14
  import { resolveClientIp } from '../http/resolve-client-ip.js';
15
15
  import { activeSpanContext } from '../shared/active-span-context.js';
16
16
  import { isInfrastructurePath } from './infrastructure-paths.js';
17
17
  import { LoggerEnvironment } from './logger.environment.js';
18
+ import { DEFAULT_SENSITIVE_QUERY_PARAMETERS, redactQueryObject, redactUrlParameters, } from './redact-query-parameters.js';
18
19
  const HTTP_STATUS_CLIENT_ERROR = 400;
19
20
  const HTTP_STATUS_SERVER_ERROR = 500;
20
21
  const isInfrastructureRequest = (url) => {
@@ -50,10 +51,19 @@ const REDACTED_LOG_PATHS = [
50
51
  'password',
51
52
  'req.headers.authorization',
52
53
  'req.headers.cookie',
54
+ 'res.headers["set-cookie"]',
53
55
  ];
54
56
  const rawSocketPort = (request) => request.raw?.socket?.remotePort;
55
57
  const remotePort = (request) => request.socket?.remotePort
56
58
  ?? rawSocketPort(request);
59
+ const withRedactedReferer = (headers, sensitiveNames) => (headers.referer === undefined
60
+ ? headers
61
+ : { ...headers, referer: redactUrlParameters(headers.referer, sensitiveNames) });
62
+ const sensitiveNamesOf = ({ sensitiveQueryParameters = [] }) => new Set([...DEFAULT_SENSITIVE_QUERY_PARAMETERS, ...sensitiveQueryParameters]);
63
+ const redactPathsOf = ({ redactPaths = [] }) => [
64
+ ...REDACTED_LOG_PATHS,
65
+ ...redactPaths,
66
+ ];
57
67
  let PinoLoggerConfig = class PinoLoggerConfig {
58
68
  environment;
59
69
  constructor(environment) {
@@ -61,7 +71,9 @@ let PinoLoggerConfig = class PinoLoggerConfig {
61
71
  }
62
72
  createOptions(options = {}) {
63
73
  const clientIp = options.resolveClientIp ?? resolveClientIp;
74
+ const sensitiveNames = sensitiveNamesOf(options);
64
75
  return {
76
+ forRoutes: [{ method: RequestMethod.ALL, path: '/{*splat}' }],
65
77
  pinoHttp: {
66
78
  autoLogging: true,
67
79
  customLogLevel: responseLogLevel,
@@ -74,17 +86,17 @@ let PinoLoggerConfig = class PinoLoggerConfig {
74
86
  level: this.environment.LOG_LEVEL,
75
87
  mixin: traceContextMixin,
76
88
  quietReqLogger: true,
77
- redact: [...REDACTED_LOG_PATHS],
89
+ redact: redactPathsOf(options),
78
90
  serializers: {
79
91
  req: (request) => ({
80
- headers: request.headers,
92
+ headers: withRedactedReferer(request.headers, sensitiveNames),
81
93
  id: request.id,
82
94
  method: request.method,
83
95
  params: request.raw?.params,
84
- query: request.query,
96
+ query: redactQueryObject(request.query, sensitiveNames),
85
97
  remoteAddress: clientIp(request),
86
98
  remotePort: remotePort(request),
87
- url: request.url,
99
+ url: redactUrlParameters(request.url, sensitiveNames),
88
100
  }),
89
101
  },
90
102
  },
@@ -0,0 +1,3 @@
1
+ export declare const DEFAULT_SENSITIVE_QUERY_PARAMETERS: readonly string[];
2
+ export declare const redactUrlParameters: (value: unknown, sensitiveNames: ReadonlySet<string>) => unknown;
3
+ export declare const redactQueryObject: (query: unknown, sensitiveNames: ReadonlySet<string>) => unknown;
@@ -0,0 +1,28 @@
1
+ export const DEFAULT_SENSITIVE_QUERY_PARAMETERS = ['code', 'token'];
2
+ const REDACTED_VALUE = '[Redacted]';
3
+ const PARAMETER_DELIMITER = /(?<delimiter>[&?])/u;
4
+ const redactParameter = (parameter, sensitiveNames) => {
5
+ const assignmentIndex = parameter.indexOf('=');
6
+ return parameter.includes('=') && sensitiveNames.has(parameter.slice(0, assignmentIndex))
7
+ ? `${parameter.slice(0, assignmentIndex + 1)}${REDACTED_VALUE}`
8
+ : parameter;
9
+ };
10
+ const redactUrl = (url, sensitiveNames) => {
11
+ const [path, ...rest] = url.split(PARAMETER_DELIMITER);
12
+ return [path, ...rest.map((part) => redactParameter(part, sensitiveNames))].join('');
13
+ };
14
+ export const redactUrlParameters = (value, sensitiveNames) => {
15
+ if (Array.isArray(value)) {
16
+ return value.map((item) => redactUrlParameters(item, sensitiveNames));
17
+ }
18
+ return typeof value === 'string' ? redactUrl(value, sensitiveNames) : value;
19
+ };
20
+ export const redactQueryObject = (query, sensitiveNames) => {
21
+ if (typeof query !== 'object' || query === null) {
22
+ return query;
23
+ }
24
+ return Object.fromEntries(Object.entries(query).map(([name, value]) => [
25
+ name,
26
+ sensitiveNames.has(name) ? REDACTED_VALUE : redactUrlParameters(value, sensitiveNames),
27
+ ]));
28
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@application-kit/observability",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Наблюдаемость NestJS-процесса: метрики Prometheus и RED-гистограммы, health-чеки и пробы, опции pino с request-id и trace-контекстом, логирование CQRS-шин, реальный IP клиента за прокси, трейсинг OpenTelemetry",
5
5
  "keywords": [
6
6
  "cqrs",
@@ -142,16 +142,16 @@
142
142
  "test:unit": "vitest run"
143
143
  },
144
144
  "dependencies": {
145
- "@application-kit/typeorm": "^0.5.1"
145
+ "@application-kit/typeorm": "^0.12.0"
146
146
  },
147
147
  "devDependencies": {
148
- "@nestjs/common": "^11.2.7",
149
- "@nestjs/core": "^11.2.7",
150
- "@nestjs/cqrs": "^11.0.3",
151
- "@nestjs/graphql": "^13.4.5",
152
- "@nestjs/platform-fastify": "^11.2.7",
148
+ "@nestjs/common": "^12.0.0",
149
+ "@nestjs/core": "^12.0.0",
150
+ "@nestjs/cqrs": "^12.0.0",
151
+ "@nestjs/graphql": "^14.0.0",
152
+ "@nestjs/platform-fastify": "^12.0.0",
153
153
  "@nestjs/terminus": "^12.1.0",
154
- "@nestjs/testing": "^11.2.7",
154
+ "@nestjs/testing": "^12.0.0",
155
155
  "@nestjs/typeorm": "^12.0.2",
156
156
  "@opentelemetry/api": "^1.10.0",
157
157
  "@opentelemetry/exporter-trace-otlp-grpc": "^0.300.0",
@@ -169,19 +169,19 @@
169
169
  "graphql": "^17.0.2",
170
170
  "import-in-the-middle": "^3.5.2",
171
171
  "nestjs-console": "^10.0.0",
172
- "nestjs-pino": "^4.6.1",
172
+ "nestjs-pino": "^5.0.0",
173
173
  "pino": "^10.3.1",
174
174
  "reflect-metadata": "^0.2.2",
175
175
  "rxjs": "^7.8.2",
176
176
  "typeorm": "^1.1.0",
177
177
  "typescript": "^6.0.3",
178
- "vitest": "^4.1.11"
178
+ "vitest": "^5.0.0"
179
179
  },
180
180
  "peerDependencies": {
181
- "@nestjs/common": "^11.0.0",
182
- "@nestjs/cqrs": "^11.0.0",
183
- "@nestjs/graphql": "^13.0.0",
184
- "@nestjs/platform-fastify": "^11.0.0",
181
+ "@nestjs/common": "^11.0.0 || ^12.0.0",
182
+ "@nestjs/cqrs": "^11.0.0 || ^12.0.0",
183
+ "@nestjs/graphql": "^13.0.0 || ^14.0.0",
184
+ "@nestjs/platform-fastify": "^11.0.0 || ^12.0.0",
185
185
  "@nestjs/terminus": "^11.0.0 || ^12.0.0",
186
186
  "@nestjs/typeorm": "^11.0.0 || ^12.0.0",
187
187
  "@opentelemetry/api": "^1.10.0",
@@ -197,7 +197,7 @@
197
197
  "graphql": "^16.0.0 || ^17.0.0",
198
198
  "import-in-the-middle": "^3.3.3",
199
199
  "nestjs-console": "^10.0.0",
200
- "nestjs-pino": "^4.6.1",
200
+ "nestjs-pino": "^4.6.1 || ^5.0.0",
201
201
  "pino": "^10.0.0",
202
202
  "typeorm": "^1.0.0"
203
203
  },