@application-kit/observability 0.9.0 → 0.11.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +26 -2
  3. package/dist/cjs/logging/index.d.ts +1 -0
  4. package/dist/cjs/logging/index.js +1 -0
  5. package/dist/cjs/logging/nest/application-logger.d.ts +5 -0
  6. package/dist/cjs/logging/nest/application-logger.js +59 -0
  7. package/dist/cjs/logging/nest/index.d.ts +1 -0
  8. package/dist/cjs/logging/nest/index.js +17 -0
  9. package/dist/cjs/logging/pino-logger-options.interface.d.ts +2 -0
  10. package/dist/cjs/logging/pino-logger.config.js +16 -4
  11. package/dist/cjs/logging/redact-query-parameters.d.ts +3 -0
  12. package/dist/cjs/logging/redact-query-parameters.js +33 -0
  13. package/dist/cjs/nest/index.d.ts +3 -0
  14. package/dist/cjs/nest/index.js +19 -0
  15. package/dist/cjs/nest/observability-root-module-options.interface.d.ts +5 -0
  16. package/dist/cjs/nest/observability-root-module-options.interface.js +2 -0
  17. package/dist/cjs/nest/observability-root.module.d.ts +5 -0
  18. package/dist/cjs/nest/observability-root.module.js +37 -0
  19. package/dist/cjs/nest/observability-web.module.d.ts +5 -0
  20. package/dist/cjs/nest/observability-web.module.js +24 -0
  21. package/dist/esm/logging/index.d.ts +1 -0
  22. package/dist/esm/logging/index.js +1 -0
  23. package/dist/esm/logging/nest/application-logger.d.ts +5 -0
  24. package/dist/esm/logging/nest/application-logger.js +23 -0
  25. package/dist/esm/logging/nest/index.d.ts +1 -0
  26. package/dist/esm/logging/nest/index.js +1 -0
  27. package/dist/esm/logging/pino-logger-options.interface.d.ts +2 -0
  28. package/dist/esm/logging/pino-logger.config.js +17 -5
  29. package/dist/esm/logging/redact-query-parameters.d.ts +3 -0
  30. package/dist/esm/logging/redact-query-parameters.js +28 -0
  31. package/dist/esm/nest/index.d.ts +3 -0
  32. package/dist/esm/nest/index.js +3 -0
  33. package/dist/esm/nest/observability-root-module-options.interface.d.ts +5 -0
  34. package/dist/esm/nest/observability-root-module-options.interface.js +1 -0
  35. package/dist/esm/nest/observability-root.module.d.ts +5 -0
  36. package/dist/esm/nest/observability-root.module.js +34 -0
  37. package/dist/esm/nest/observability-web.module.d.ts +5 -0
  38. package/dist/esm/nest/observability-web.module.js +21 -0
  39. package/package.json +36 -16
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @application-kit/observability
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 523d0e9: Строка лога о запросе больше не содержит секретов: `set-cookie` ответа заменяется маркером, значения параметров `token` и `code` вырезаются из `req.url`, `req.query` и заголовка `referer`, в том числе внутри другого параметра. Новые опции логгера `redactPaths` и `sensitiveQueryParameters` дополняют дефолт пакета.
8
+
9
+ ### Patch Changes
10
+
11
+ - 8e10f98: Peer-зависимости принимают Nest 12, `@nestjs/graphql` и `@nestjs/mercurius` 14, `@nestjs/cqrs` 12 и `nestjs-pino` 5. `PinoLoggerConfig` задаёт `forRoutes` явно: с маршрутом nestjs-pino 5 по умолчанию запросы на Fastify не логировались.
12
+ - Updated dependencies [8e10f98]
13
+ - @application-kit/typeorm@0.5.3
14
+
15
+ ## 0.10.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 3a17e1a: Корень наблюдаемости одним вызовом: вход `/nest` — `ObservabilityRootModule.forRoot({ imports, resolveClientIp })` (pino, логгер приложения под `OBSERVABILITY_LOGGER`, ядро health-чека) и `ObservabilityWebModule.forRoot({ publicRoute })` (HTTP-носители `/health` и `/metrics`). Вход `/logging/nest` — класс `ApplicationLogger`.
20
+
3
21
  ## 0.9.0
4
22
 
5
23
  ### Minor Changes
package/README.md CHANGED
@@ -23,11 +23,35 @@ Peer-зависимости: `@nestjs/common`, `@prometheus-io/client`; оста
23
23
  | `@application-kit/observability/health/cli` | CLI-команда `<группа> health` | `/health` + `nestjs-console` |
24
24
  | `@application-kit/observability/http` | `resolveClientIp`, `resolveRequestFacts`, `firstHeaderValue` — IP клиента за прокси и факты запроса | ничего |
25
25
  | `@application-kit/observability/logging` | опции nestjs-pino, request-id, support-id, служебные пути, токен логгера | `@opentelemetry/api`, `class-validator`, `class-transformer`; типы — `nestjs-pino`, `pino` |
26
+ | `@application-kit/observability/logging/nest` | `ApplicationLogger` — `PinoLogger` с контекстом приложения | `@nestjs/common`, `nestjs-pino` |
26
27
  | `@application-kit/observability/metrics` | реестр, гистограммы HTTP и GraphQL, `GET /metrics`, хуки Fastify, сервер `/metrics` | `@nestjs/common`, `@prometheus-io/client`; типы — `fastify`, `@nestjs/platform-fastify` |
28
+ | `@application-kit/observability/nest` | `ObservabilityRootModule`, `ObservabilityWebModule` — сборка логирования, health и метрик | `nestjs-pino`, `pino` + зависимости `/health`, `/logging`, `/metrics` |
27
29
  | `@application-kit/observability/tracing` | `startTracing` — OpenTelemetry NodeSDK для `node --import`, только ESM | `@opentelemetry/sdk-node`, `@opentelemetry/instrumentation`, `@opentelemetry/instrumentation-http`, `@opentelemetry/exporter-trace-otlp-grpc`, `@opentelemetry/semantic-conventions` |
28
30
 
29
31
  Корневого входа нет: каждый вход несёт свои peer-зависимости, и лишнего потребитель не ставит.
30
32
 
33
+ ## Сборка процесса (`/nest`)
34
+
35
+ `ObservabilityRootModule.forRoot` — глобальный корень на процесс: `LoggerModule` nestjs-pino, `ApplicationLogger` под
36
+ `OBSERVABILITY_LOGGER`, ядро health-чека с логгером pino в terminus. `ObservabilityWebModule.forRoot` — только в
37
+ web-графе: HTTP-носители `/health` и `/metrics`. `HealthGraphqlModule` подключается отдельно: его вход требует `@nestjs/graphql`.
38
+
39
+ ```ts
40
+ @Module({
41
+ imports: [
42
+ ObservabilityRootModule.forRoot({
43
+ imports: [healthConfigsModule, loggerConfigsModule],
44
+ resolveClientIp: resolveRequestClientIp,
45
+ }),
46
+ ObservabilityWebModule.forRoot({ publicRoute: Public() }),
47
+ HealthGraphqlModule.forRoot({ publicRoute: Public() }),
48
+ ],
49
+ })
50
+ export class WebModule {}
51
+ ```
52
+
53
+ `resolveClientIp` необязателен: дефолт и доверенные заголовки — раздел «IP клиента за прокси».
54
+
31
55
  ## Метрики
32
56
 
33
57
  - `MetricsRegistry` — провайдер с собственным `Registry` (не глобальным: экземпляры независимы) и метриками процесса
@@ -109,7 +133,7 @@ export class AppModule {}
109
133
 
110
134
  ## Health-чек (`/health`)
111
135
 
112
- `HealthModule.forRoot({ imports, logger })` — глобальное ядро процесса: terminus и `HealthChecks` (ping БД, heap, RSS,
136
+ `HealthModule.forRoot({ imports, terminus })` — глобальное ядро процесса: terminus и `HealthChecks` (ping БД, heap, RSS,
113
137
  диск по корню; пороги — `HealthConfig`/`HealthEnvironment`). Носители подключаются там, где нужны, и берут состав
114
138
  отсюда — вердикт не зависит от того, каким путём спросили:
115
139
 
@@ -125,7 +149,7 @@ export class AppModule {}
125
149
  ```ts
126
150
  @Module({
127
151
  imports: [
128
- HealthModule.forRoot({ imports: [healthConfigsModule], logger: Logger }),
152
+ HealthModule.forRoot({ imports: [healthConfigsModule], terminus: { logger: Logger } }),
129
153
  HealthHttpModule.forRoot({ publicRoute: Public() }),
130
154
  HealthGraphqlModule.forRoot({ publicRoute: Public() }),
131
155
  ],
@@ -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);
@@ -0,0 +1,5 @@
1
+ import * as nestjsPino from 'nestjs-pino';
2
+ import type { ObservabilityLoggerInterface } from '../observability-logger.interface.js';
3
+ export declare class ApplicationLogger extends nestjsPino.PinoLogger implements ObservabilityLoggerInterface {
4
+ constructor(options: nestjsPino.Params);
5
+ }
@@ -0,0 +1,59 @@
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 __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ 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;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ var __metadata = (this && this.__metadata) || function (k, v) {
42
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
43
+ };
44
+ var ApplicationLogger_1;
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.ApplicationLogger = void 0;
47
+ const common_1 = require("@nestjs/common");
48
+ const nestjsPino = __importStar(require("nestjs-pino"));
49
+ let ApplicationLogger = ApplicationLogger_1 = class ApplicationLogger extends nestjsPino.PinoLogger {
50
+ constructor(options) {
51
+ super(options);
52
+ this.setContext(ApplicationLogger_1.name);
53
+ }
54
+ };
55
+ exports.ApplicationLogger = ApplicationLogger;
56
+ exports.ApplicationLogger = ApplicationLogger = ApplicationLogger_1 = __decorate([
57
+ (0, common_1.Injectable)(),
58
+ __metadata("design:paramtypes", [Object])
59
+ ], ApplicationLogger);
@@ -0,0 +1 @@
1
+ export * from './application-logger.js';
@@ -0,0 +1,17 @@
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./application-logger.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;
@@ -0,0 +1,3 @@
1
+ export * from './observability-root-module-options.interface.js';
2
+ export * from './observability-root.module.js';
3
+ export * from './observability-web.module.js';
@@ -0,0 +1,19 @@
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./observability-root-module-options.interface.js"), exports);
18
+ __exportStar(require("./observability-root.module.js"), exports);
19
+ __exportStar(require("./observability-web.module.js"), exports);
@@ -0,0 +1,5 @@
1
+ import type { ModuleMetadata } from '@nestjs/common';
2
+ import type { PinoLoggerOptionsInterface } from '../logging/pino-logger-options.interface.js';
3
+ export interface ObservabilityRootModuleOptionsInterface extends PinoLoggerOptionsInterface {
4
+ readonly imports: NonNullable<ModuleMetadata['imports']>;
5
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ import type { DynamicModule } from '@nestjs/common';
2
+ import type { ObservabilityRootModuleOptionsInterface } from './observability-root-module-options.interface.js';
3
+ export declare class ObservabilityRootModule {
4
+ static forRoot({ imports, ...loggerOptions }: ObservabilityRootModuleOptionsInterface): DynamicModule;
5
+ }
@@ -0,0 +1,37 @@
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.ObservabilityRootModule = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const nestjs_pino_1 = require("nestjs-pino");
12
+ const health_module_js_1 = require("../health/health.module.js");
13
+ const constants_js_1 = require("../logging/constants.js");
14
+ const application_logger_js_1 = require("../logging/nest/application-logger.js");
15
+ const pino_logger_config_js_1 = require("../logging/pino-logger.config.js");
16
+ let ObservabilityRootModule = class ObservabilityRootModule {
17
+ static forRoot({ imports, ...loggerOptions }) {
18
+ return {
19
+ exports: [application_logger_js_1.ApplicationLogger, constants_js_1.OBSERVABILITY_LOGGER],
20
+ global: true,
21
+ imports: [
22
+ nestjs_pino_1.LoggerModule.forRootAsync({
23
+ imports,
24
+ inject: [pino_logger_config_js_1.PinoLoggerConfig],
25
+ useFactory: (config) => config.createOptions(loggerOptions),
26
+ }),
27
+ health_module_js_1.HealthModule.forRoot({ imports, terminus: { logger: nestjs_pino_1.Logger } }),
28
+ ],
29
+ module: this,
30
+ providers: [application_logger_js_1.ApplicationLogger, { provide: constants_js_1.OBSERVABILITY_LOGGER, useExisting: application_logger_js_1.ApplicationLogger }],
31
+ };
32
+ }
33
+ };
34
+ exports.ObservabilityRootModule = ObservabilityRootModule;
35
+ exports.ObservabilityRootModule = ObservabilityRootModule = __decorate([
36
+ (0, common_1.Module)({})
37
+ ], ObservabilityRootModule);
@@ -0,0 +1,5 @@
1
+ import type { DynamicModule } from '@nestjs/common';
2
+ import type { HealthCarrierOptionsInterface } from '../health/health-module-options.interface.js';
3
+ export declare class ObservabilityWebModule {
4
+ static forRoot(options?: HealthCarrierOptionsInterface): DynamicModule;
5
+ }
@@ -0,0 +1,24 @@
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.ObservabilityWebModule = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const health_http_module_js_1 = require("../health/health-http.module.js");
12
+ const metrics_http_module_js_1 = require("../metrics/metrics-http.module.js");
13
+ let ObservabilityWebModule = class ObservabilityWebModule {
14
+ static forRoot(options = {}) {
15
+ return {
16
+ imports: [health_http_module_js_1.HealthHttpModule.forRoot(options), metrics_http_module_js_1.MetricsHttpModule.forRoot(options)],
17
+ module: this,
18
+ };
19
+ }
20
+ };
21
+ exports.ObservabilityWebModule = ObservabilityWebModule;
22
+ exports.ObservabilityWebModule = ObservabilityWebModule = __decorate([
23
+ (0, common_1.Module)({})
24
+ ], ObservabilityWebModule);
@@ -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';
@@ -0,0 +1,5 @@
1
+ import * as nestjsPino from 'nestjs-pino';
2
+ import type { ObservabilityLoggerInterface } from '../observability-logger.interface.js';
3
+ export declare class ApplicationLogger extends nestjsPino.PinoLogger implements ObservabilityLoggerInterface {
4
+ constructor(options: nestjsPino.Params);
5
+ }
@@ -0,0 +1,23 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ 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;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var ApplicationLogger_1;
11
+ import { Injectable } from '@nestjs/common';
12
+ import * as nestjsPino from 'nestjs-pino';
13
+ let ApplicationLogger = ApplicationLogger_1 = class ApplicationLogger extends nestjsPino.PinoLogger {
14
+ constructor(options) {
15
+ super(options);
16
+ this.setContext(ApplicationLogger_1.name);
17
+ }
18
+ };
19
+ ApplicationLogger = ApplicationLogger_1 = __decorate([
20
+ Injectable(),
21
+ __metadata("design:paramtypes", [Object])
22
+ ], ApplicationLogger);
23
+ export { ApplicationLogger };
@@ -0,0 +1 @@
1
+ export * from './application-logger.js';
@@ -0,0 +1 @@
1
+ export * from './application-logger.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
+ };
@@ -0,0 +1,3 @@
1
+ export * from './observability-root-module-options.interface.js';
2
+ export * from './observability-root.module.js';
3
+ export * from './observability-web.module.js';
@@ -0,0 +1,3 @@
1
+ export * from './observability-root-module-options.interface.js';
2
+ export * from './observability-root.module.js';
3
+ export * from './observability-web.module.js';
@@ -0,0 +1,5 @@
1
+ import type { ModuleMetadata } from '@nestjs/common';
2
+ import type { PinoLoggerOptionsInterface } from '../logging/pino-logger-options.interface.js';
3
+ export interface ObservabilityRootModuleOptionsInterface extends PinoLoggerOptionsInterface {
4
+ readonly imports: NonNullable<ModuleMetadata['imports']>;
5
+ }
@@ -0,0 +1,5 @@
1
+ import type { DynamicModule } from '@nestjs/common';
2
+ import type { ObservabilityRootModuleOptionsInterface } from './observability-root-module-options.interface.js';
3
+ export declare class ObservabilityRootModule {
4
+ static forRoot({ imports, ...loggerOptions }: ObservabilityRootModuleOptionsInterface): DynamicModule;
5
+ }
@@ -0,0 +1,34 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ 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;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Module } from '@nestjs/common';
8
+ import { Logger, LoggerModule } from 'nestjs-pino';
9
+ import { HealthModule } from '../health/health.module.js';
10
+ import { OBSERVABILITY_LOGGER } from '../logging/constants.js';
11
+ import { ApplicationLogger } from '../logging/nest/application-logger.js';
12
+ import { PinoLoggerConfig } from '../logging/pino-logger.config.js';
13
+ let ObservabilityRootModule = class ObservabilityRootModule {
14
+ static forRoot({ imports, ...loggerOptions }) {
15
+ return {
16
+ exports: [ApplicationLogger, OBSERVABILITY_LOGGER],
17
+ global: true,
18
+ imports: [
19
+ LoggerModule.forRootAsync({
20
+ imports,
21
+ inject: [PinoLoggerConfig],
22
+ useFactory: (config) => config.createOptions(loggerOptions),
23
+ }),
24
+ HealthModule.forRoot({ imports, terminus: { logger: Logger } }),
25
+ ],
26
+ module: this,
27
+ providers: [ApplicationLogger, { provide: OBSERVABILITY_LOGGER, useExisting: ApplicationLogger }],
28
+ };
29
+ }
30
+ };
31
+ ObservabilityRootModule = __decorate([
32
+ Module({})
33
+ ], ObservabilityRootModule);
34
+ export { ObservabilityRootModule };
@@ -0,0 +1,5 @@
1
+ import type { DynamicModule } from '@nestjs/common';
2
+ import type { HealthCarrierOptionsInterface } from '../health/health-module-options.interface.js';
3
+ export declare class ObservabilityWebModule {
4
+ static forRoot(options?: HealthCarrierOptionsInterface): DynamicModule;
5
+ }
@@ -0,0 +1,21 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ 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;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Module } from '@nestjs/common';
8
+ import { HealthHttpModule } from '../health/health-http.module.js';
9
+ import { MetricsHttpModule } from '../metrics/metrics-http.module.js';
10
+ let ObservabilityWebModule = class ObservabilityWebModule {
11
+ static forRoot(options = {}) {
12
+ return {
13
+ imports: [HealthHttpModule.forRoot(options), MetricsHttpModule.forRoot(options)],
14
+ module: this,
15
+ };
16
+ }
17
+ };
18
+ ObservabilityWebModule = __decorate([
19
+ Module({})
20
+ ], ObservabilityWebModule);
21
+ export { ObservabilityWebModule };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@application-kit/observability",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Наблюдаемость NestJS-процесса: метрики Prometheus и RED-гистограммы, health-чеки и пробы, опции pino с request-id и trace-контекстом, логирование CQRS-шин, реальный IP клиента за прокси, трейсинг OpenTelemetry",
5
5
  "keywords": [
6
6
  "cqrs",
@@ -89,6 +89,16 @@
89
89
  "default": "./dist/cjs/logging/index.js"
90
90
  }
91
91
  },
92
+ "./logging/nest": {
93
+ "import": {
94
+ "types": "./dist/esm/logging/nest/index.d.ts",
95
+ "default": "./dist/esm/logging/nest/index.js"
96
+ },
97
+ "require": {
98
+ "types": "./dist/cjs/logging/nest/index.d.ts",
99
+ "default": "./dist/cjs/logging/nest/index.js"
100
+ }
101
+ },
92
102
  "./metric-names.json": "./metric-names.json",
93
103
  "./metrics": {
94
104
  "import": {
@@ -100,6 +110,16 @@
100
110
  "default": "./dist/cjs/metrics/index.js"
101
111
  }
102
112
  },
113
+ "./nest": {
114
+ "import": {
115
+ "types": "./dist/esm/nest/index.d.ts",
116
+ "default": "./dist/esm/nest/index.js"
117
+ },
118
+ "require": {
119
+ "types": "./dist/cjs/nest/index.d.ts",
120
+ "default": "./dist/cjs/nest/index.js"
121
+ }
122
+ },
103
123
  "./package.json": "./package.json",
104
124
  "./tracing": {
105
125
  "import": {
@@ -116,22 +136,22 @@
116
136
  "metric-names.json"
117
137
  ],
118
138
  "scripts": {
119
- "build": "rm -rf ./dist && node ../../scripts/barrels.mjs create ./src/cqrs && node ../../scripts/barrels.mjs create ./src/health --entry-only=cli,graphql && node ../../scripts/barrels.mjs create ./src/http && node ../../scripts/barrels.mjs create ./src/logging && node ../../scripts/barrels.mjs create ./src/metrics && node ../../scripts/barrels.mjs create ./src/tracing && tsc --project tsconfig.build.json && tsc --project tsconfig.build.cjs.json && node ../../scripts/barrels.mjs clean ./src && node ../../scripts/mark-cjs.mjs ./dist/cjs",
139
+ "build": "rm -rf ./dist && node ../../scripts/barrels.mjs create ./src/cqrs && node ../../scripts/barrels.mjs create ./src/health --entry-only=cli,graphql && node ../../scripts/barrels.mjs create ./src/http && node ../../scripts/barrels.mjs create ./src/logging --entry-only=nest && node ../../scripts/barrels.mjs create ./src/metrics && node ../../scripts/barrels.mjs create ./src/nest && node ../../scripts/barrels.mjs create ./src/tracing && tsc --project tsconfig.build.json && tsc --project tsconfig.build.cjs.json && node ../../scripts/barrels.mjs clean ./src && node ../../scripts/mark-cjs.mjs ./dist/cjs",
120
140
  "lint:tsc": "tsc --noEmit --pretty --project .",
121
141
  "test:mutation": "stryker run",
122
142
  "test:unit": "vitest run"
123
143
  },
124
144
  "dependencies": {
125
- "@application-kit/typeorm": "^0.5.1"
145
+ "@application-kit/typeorm": "^0.5.3"
126
146
  },
127
147
  "devDependencies": {
128
- "@nestjs/common": "^11.2.7",
129
- "@nestjs/core": "^11.2.7",
130
- "@nestjs/cqrs": "^11.0.3",
131
- "@nestjs/graphql": "^13.4.5",
132
- "@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",
133
153
  "@nestjs/terminus": "^12.1.0",
134
- "@nestjs/testing": "^11.2.7",
154
+ "@nestjs/testing": "^12.0.0",
135
155
  "@nestjs/typeorm": "^12.0.2",
136
156
  "@opentelemetry/api": "^1.10.0",
137
157
  "@opentelemetry/exporter-trace-otlp-grpc": "^0.300.0",
@@ -149,19 +169,19 @@
149
169
  "graphql": "^17.0.2",
150
170
  "import-in-the-middle": "^3.5.2",
151
171
  "nestjs-console": "^10.0.0",
152
- "nestjs-pino": "^4.6.1",
172
+ "nestjs-pino": "^5.0.0",
153
173
  "pino": "^10.3.1",
154
174
  "reflect-metadata": "^0.2.2",
155
175
  "rxjs": "^7.8.2",
156
176
  "typeorm": "^1.1.0",
157
177
  "typescript": "^6.0.3",
158
- "vitest": "^4.1.11"
178
+ "vitest": "^5.0.0"
159
179
  },
160
180
  "peerDependencies": {
161
- "@nestjs/common": "^11.0.0",
162
- "@nestjs/cqrs": "^11.0.0",
163
- "@nestjs/graphql": "^13.0.0",
164
- "@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",
165
185
  "@nestjs/terminus": "^11.0.0 || ^12.0.0",
166
186
  "@nestjs/typeorm": "^11.0.0 || ^12.0.0",
167
187
  "@opentelemetry/api": "^1.10.0",
@@ -177,7 +197,7 @@
177
197
  "graphql": "^16.0.0 || ^17.0.0",
178
198
  "import-in-the-middle": "^3.3.3",
179
199
  "nestjs-console": "^10.0.0",
180
- "nestjs-pino": "^4.6.1",
200
+ "nestjs-pino": "^4.6.1 || ^5.0.0",
181
201
  "pino": "^10.0.0",
182
202
  "typeorm": "^1.0.0"
183
203
  },