@yolo-croket-dev/core 1.10.0-jun.2-beta.1 → 1.10.1

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,7 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.10.0
4
+
5
+ - 비회원 인증 로그 마스킹·검증 오류 보호·재시도 정보 필드를 정식 발행합니다.
6
+ - 기존 1.9.14의 AMQP timeout, ACK/NACK 옵션, RabbitMQ 연결 이름 설정을 보존합니다.
7
+ - 소비 서비스에서 사용하던 1.10.0-jun.2의 gRPC metadata·예외 처리와 포트 파싱을 유지합니다.
8
+
3
9
  ## 1.10.0-jun.2-beta.1
4
10
 
5
11
  - 현재 소비 버전 1.10.0-jun.2의 gRPC metadata 및 예외 처리 동작을 유지합니다.
6
12
  - 비회원 인증 요청 로그의 민감 값을 마스킹하고 검증 오류에서 입력값을 제외합니다.
7
13
  - 인증 실패의 남은 입력 횟수와 재시도 대기시간 필드를 추가합니다.
14
+
15
+ ## 1.9.13-beta.1
16
+
17
+ - AMQP 요청의 `param.timeout`을 최대 29분 범위에서 적용해 timeout 시 클라이언트 Observable 구독을 해제합니다.
18
+
19
+ ## 1.9.12-beta.1
20
+
21
+ - `GracefulServerRMQ`가 RabbitMQ 연결에 queue, environment, owner, hostname, process ID 기반 `connection_name`을 자동으로 추가합니다.
22
+ - 호출자가 지정한 `connectionOptions`, `clientProperties`, 유효한 문자열 `connection_name`은 그대로 보존합니다.
23
+ - 문자열이 아닌 `connection_name`은 런타임 오류 없이 자동 생성값으로 대체합니다.
@@ -152,7 +152,7 @@ let GrpcExceptionFilter = class GrpcExceptionFilter extends microservices_1.Base
152
152
  }
153
153
  }
154
154
  };
155
- GrpcExceptionFilter = __decorate([
155
+ exports.GrpcExceptionFilter = GrpcExceptionFilter;
156
+ exports.GrpcExceptionFilter = GrpcExceptionFilter = __decorate([
156
157
  (0, common_1.Catch)()
157
158
  ], GrpcExceptionFilter);
158
- exports.GrpcExceptionFilter = GrpcExceptionFilter;
@@ -25,6 +25,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.AmqpManagement = void 0;
27
27
  const msa_logger_1 = __importStar(require("@yolo-croket-dev/msa-logger"));
28
+ const rmq_diagnostics_function_1 = require("./rmq-diagnostics.function");
28
29
  const _1 = require(".");
29
30
  const msaLogger = new msa_logger_1.default(msa_logger_1.ServerTypeEnum.SERVICE);
30
31
  class AmqpManagement {
@@ -44,7 +45,9 @@ class AmqpManagement {
44
45
  }
45
46
  return param;
46
47
  }
47
- async call(funcName, messagePattern, payload, argParam, options, file) {
48
+ async call(funcName, messagePattern, payload, argParam, options,
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ file) {
48
51
  const param = this.paramProcessor(argParam || {
49
52
  traceId: (0, _1.generateTraceId)(),
50
53
  isInternal: true,
@@ -58,7 +61,9 @@ class AmqpManagement {
58
61
  return result.data;
59
62
  }
60
63
  catch (err) {
61
- msaLogger.warn(`ThrowWrapppingCall[${funcName}] error: traceId=${param.traceId}`, { traceId: param.traceId }, { param });
64
+ if (!(0, rmq_diagnostics_function_1.getRmqDiagnosticSnapshot)(this.client) && !(0, rmq_diagnostics_function_1.isRmqDiagnosticsEnabled)()) {
65
+ msaLogger.warn(`ThrowWrapppingCall[${funcName}] error: traceId=${param.traceId}`, { traceId: param.traceId }, { param });
66
+ }
62
67
  if (options && options.isErrorThrowing) {
63
68
  throw err;
64
69
  }
@@ -1,10 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AmqpHandler = exports.HttpExceptionCustom = void 0;
4
+ /* eslint-disable max-len */
5
+ /* eslint-disable @typescript-eslint/no-explicit-any */
6
+ const microservices_1 = require("@nestjs/microservices");
4
7
  const common_1 = require("@nestjs/common");
5
8
  const rxjs_1 = require("rxjs");
6
9
  const operators_1 = require("rxjs/operators");
7
10
  const msa_logger_1 = require("@yolo-croket-dev/msa-logger");
11
+ const rmq_diagnostics_function_1 = require("./rmq-diagnostics.function");
8
12
  const _1 = require(".");
9
13
  const DEFAULT_TIMEOUT = 1000 * 60 * 29; // 29분 타임아웃
10
14
  const DEFAULT_TIMEOUT_MESSAGE = '요청이 타임아웃되었습니다.';
@@ -17,11 +21,31 @@ class HttpExceptionCustom extends common_1.HttpException {
17
21
  }
18
22
  }
19
23
  exports.HttpExceptionCustom = HttpExceptionCustom;
24
+ function resolveAmqpTimeout(timeoutMs) {
25
+ if (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
26
+ return DEFAULT_TIMEOUT;
27
+ }
28
+ return Math.min(timeoutMs, DEFAULT_TIMEOUT);
29
+ }
30
+ function getLegacyRmqQueue(client) {
31
+ if (!(client instanceof microservices_1.ClientRMQ))
32
+ return undefined;
33
+ try {
34
+ const queue = Reflect.get(Reflect.get(client, 'options'), 'queue');
35
+ return typeof queue === 'string' ? (0, msa_logger_1.sanitizeDiagnosticText)(queue).slice(0, 240) : undefined;
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
20
41
  async function AmqpHandler(client, messagePattern, payload) {
21
- var _a, _b, _c, _d;
42
+ var _a, _b, _c, _d, _e;
22
43
  let data;
44
+ const timeoutMs = resolveAmqpTimeout((_a = payload === null || payload === void 0 ? void 0 : payload.param) === null || _a === void 0 ? void 0 : _a.timeout);
45
+ const startedAt = Date.now();
46
+ const startConnection = (0, rmq_diagnostics_function_1.getRmqDiagnosticSnapshot)(client);
23
47
  try {
24
- data = await (0, rxjs_1.lastValueFrom)(client.send(messagePattern, payload).pipe((0, operators_1.timeout)(DEFAULT_TIMEOUT)));
48
+ data = await (0, rxjs_1.lastValueFrom)(client.send(messagePattern, payload).pipe((0, operators_1.timeout)(timeoutMs)));
25
49
  if (data.isError) {
26
50
  const status = data.status || 500;
27
51
  const subMessage = data.subMessage;
@@ -29,7 +53,7 @@ async function AmqpHandler(client, messagePattern, payload) {
29
53
  * 파일 객체 처리
30
54
  * 다른 서비스에서 이미 처리가 될 수 있음
31
55
  * */
32
- (0, _1.reWriteFileValue)((_a = data === null || data === void 0 ? void 0 : data.arg) === null || _a === void 0 ? void 0 : _a.file);
56
+ (0, _1.reWriteFileValue)((_b = data === null || data === void 0 ? void 0 : data.arg) === null || _b === void 0 ? void 0 : _b.file);
33
57
  const errInfo = data.errInfo ? data.errInfo : {
34
58
  message: data.errMessage,
35
59
  type: data.errType,
@@ -48,36 +72,67 @@ async function AmqpHandler(client, messagePattern, payload) {
48
72
  }
49
73
  }
50
74
  catch (err) {
75
+ const normalized = (startConnection || (0, rmq_diagnostics_function_1.isRmqDiagnosticsEnabled)())
76
+ ? (0, msa_logger_1.normalizeDiagnosticError)(err, err instanceof HttpExceptionCustom ? 'service_error' : undefined) : undefined;
77
+ const diagnosticInfo = normalized ? {
78
+ ...normalized.diagnosticInfo,
79
+ ...(0, rmq_diagnostics_function_1.getRmqDiagnosticSnapshot)(client),
80
+ ...(startConnection ? {} : {
81
+ direction: 'client',
82
+ connectionState: 'unobserved',
83
+ targetService: getLegacyRmqQueue(client),
84
+ }),
85
+ event: 'call_failed',
86
+ messagePattern: (0, msa_logger_1.sanitizeDiagnosticText)(messagePattern).slice(0, 240),
87
+ elapsedMs: Date.now() - startedAt,
88
+ timeoutMs,
89
+ startGeneration: startConnection === null || startConnection === void 0 ? void 0 : startConnection.generation,
90
+ startConnectionState: startConnection === null || startConnection === void 0 ? void 0 : startConnection.connectionState,
91
+ } : undefined;
92
+ const details = normalized ? normalized.details : { err };
93
+ if (normalized)
94
+ (0, rmq_diagnostics_function_1.recordRmqDiagnosticError)(err, normalized);
51
95
  if (typeof err === 'string' && err.includes('There is no matching message handler defined in the remote service.')) {
52
96
  msaLogger.error('NOT_EXISTS_MESSAGE_PATTERN', {
53
- traceId: ((_b = payload === null || payload === void 0 ? void 0 : payload.param) === null || _b === void 0 ? void 0 : _b.traceId) || '-',
97
+ traceId: ((_c = payload === null || payload === void 0 ? void 0 : payload.param) === null || _c === void 0 ? void 0 : _c.traceId) || '-',
98
+ diagnosticInfo,
54
99
  errInfo: {
55
100
  errType: 'NOT_EXISTS_MESSAGE_PATTERN',
56
101
  message: `${messagePattern} 해당 메시지 패턴은 존재하지 않습니다.`,
57
102
  status: 404,
58
103
  },
59
- }, { err });
60
- throw new HttpExceptionCustom('해당 메시지 패턴은 존재하지 않습니다.', 404, 'There is no matching message handler defined in the remote service.', {
104
+ }, details);
105
+ const exception = new HttpExceptionCustom('해당 메시지 패턴은 존재하지 않습니다.', 404, 'There is no matching message handler defined in the remote service.', {
61
106
  type: 'NOT_EXISTS_MESSAGE_PATTERN',
62
107
  message: `${messagePattern} 해당 메시지 패턴은 존재하지 않습니다.`,
63
108
  });
109
+ if (normalized)
110
+ (0, rmq_diagnostics_function_1.recordRmqDiagnosticError)(exception, normalized);
111
+ throw exception;
64
112
  }
65
- else if (err instanceof rxjs_1.TimeoutError || err.message === DEFAULT_TIMEOUT_MESSAGE) {
113
+ else if (err instanceof rxjs_1.TimeoutError || (err === null || err === void 0 ? void 0 : err.message) === DEFAULT_TIMEOUT_MESSAGE) {
66
114
  msaLogger.error('TIMEOUT_ERROR', {
67
- traceId: ((_c = payload === null || payload === void 0 ? void 0 : payload.param) === null || _c === void 0 ? void 0 : _c.traceId) || '-',
115
+ traceId: ((_d = payload === null || payload === void 0 ? void 0 : payload.param) === null || _d === void 0 ? void 0 : _d.traceId) || '-',
116
+ diagnosticInfo,
68
117
  errInfo: {
69
118
  errType: 'TIMEOUT_ERROR',
70
119
  message: DEFAULT_TIMEOUT_MESSAGE,
71
120
  status: 500,
72
121
  },
73
- }, { err });
74
- throw new HttpExceptionCustom(DEFAULT_TIMEOUT_MESSAGE, 500, DEFAULT_TIMEOUT_MESSAGE, {
122
+ }, details);
123
+ const exception = new HttpExceptionCustom(DEFAULT_TIMEOUT_MESSAGE, 500, DEFAULT_TIMEOUT_MESSAGE, {
75
124
  type: 'TIMEOUT_ERROR',
76
125
  message: DEFAULT_TIMEOUT_MESSAGE,
77
126
  });
127
+ if (normalized)
128
+ (0, rmq_diagnostics_function_1.recordRmqDiagnosticError)(exception, normalized);
129
+ throw exception;
78
130
  }
79
131
  else {
80
- msaLogger.warn('AMQP_HANDLER_ERROR', { traceId: ((_d = payload === null || payload === void 0 ? void 0 : payload.param) === null || _d === void 0 ? void 0 : _d.traceId) || '-' }, { err });
132
+ msaLogger.warn('AMQP_HANDLER_ERROR', {
133
+ traceId: ((_e = payload === null || payload === void 0 ? void 0 : payload.param) === null || _e === void 0 ? void 0 : _e.traceId) || '-',
134
+ diagnosticInfo,
135
+ }, details);
81
136
  throw err;
82
137
  }
83
138
  }
@@ -1,7 +1,7 @@
1
1
  import { Metadata } from '@grpc/grpc-js';
2
2
  import { AbstractParam } from '../interfaces';
3
3
  export declare const GRPC_PARAM_METADATA_KEY = "param";
4
- export declare type GrpcParamMetadata = AbstractParam;
4
+ export type GrpcParamMetadata = AbstractParam;
5
5
  /**
6
6
  * @author jun
7
7
  * @description gRPC metadata에 AMQP param 호환 컨텍스트를 저장합니다.
@@ -8,3 +8,4 @@ export * from './extract-redis-url.function';
8
8
  export * from './abort';
9
9
  export * from './uncaught-exception-handler';
10
10
  export * from './unhandled-rejection-handler';
11
+ export * from './rmq-diagnostics.function';
@@ -24,3 +24,4 @@ __exportStar(require("./extract-redis-url.function"), exports);
24
24
  __exportStar(require("./abort"), exports);
25
25
  __exportStar(require("./uncaught-exception-handler"), exports);
26
26
  __exportStar(require("./unhandled-rejection-handler"), exports);
27
+ __exportStar(require("./rmq-diagnostics.function"), exports);
@@ -0,0 +1,89 @@
1
+ /// <reference types="node" />
2
+ import { EventEmitter } from 'node:events';
3
+ import { ClientRMQ, RmqOptions } from '@nestjs/microservices';
4
+ import { DiagnosticInfo, NormalizedDiagnosticError } from '@yolo-croket-dev/msa-logger';
5
+ type RmqConnectionOptions = NonNullable<RmqOptions['options']>;
6
+ type Snapshot = Pick<DiagnosticInfo, 'connectionId' | 'connectionName' | 'direction' | 'targetService' | 'generation' | 'connectionState' | 'stateChangedAt'>;
7
+ type DiagnosticWriter = (info: DiagnosticInfo, details?: NormalizedDiagnosticError['details']) => void;
8
+ /**
9
+ * @author jun
10
+ * @description 프로세스의 RMQ 연결 진단 활성 여부를 확인한다.
11
+ * @process 명시적인 true 값만 활성으로 해석한다.
12
+ */
13
+ export declare function isRmqDiagnosticsEnabled(): boolean;
14
+ /**
15
+ * @author jun
16
+ * @description 전송 동작을 바꾸지 않고 연결 상태와 반복 실패를 기록한다.
17
+ * @process 연결 이벤트 구독 → 상태 갱신/60초 실패 요약 → 종료 시 listener/timer 정리.
18
+ */
19
+ export declare class RmqConnectionObserver {
20
+ private readonly direction;
21
+ private readonly write;
22
+ readonly connectionId: `${string}-${string}-${string}-${string}-${string}`;
23
+ readonly connectionName: string;
24
+ readonly targetService: string;
25
+ private generation;
26
+ private state;
27
+ private changedAt;
28
+ private emitter?;
29
+ private listeners;
30
+ private timer?;
31
+ private failed;
32
+ private repeatCount;
33
+ private firstOccurredAt?;
34
+ private lastOccurredAt?;
35
+ private latestFailure?;
36
+ constructor(queue: string | undefined, direction: 'client' | 'consumer', configuredName?: string, write?: DiagnosticWriter);
37
+ snapshot(): Snapshot;
38
+ private transition;
39
+ private emit;
40
+ private flush;
41
+ private onFailure;
42
+ observe(emitter: EventEmitter): void;
43
+ private detach;
44
+ close(): void;
45
+ }
46
+ /**
47
+ * @author jun
48
+ * @description 기존 연결 옵션으로 관찰자를 만든다.
49
+ * @process 명시적인 broker 연결 이름을 읽고 인스턴스 식별자를 생성한다.
50
+ */
51
+ export declare function createRmqObserver(options: RmqConnectionOptions, direction: 'client' | 'consumer'): RmqConnectionObserver;
52
+ /**
53
+ * @author jun
54
+ * @description 기존 RMQ 옵션에 연결 이름만 추가한다.
55
+ * @process 원본 옵션과 명시적인 연결 이름을 보존한 복사본을 반환한다.
56
+ */
57
+ export declare function withRmqConnectionName<T extends RmqConnectionOptions>(options: T, observer: RmqConnectionObserver): T;
58
+ /**
59
+ * @author jun
60
+ * @description 클라이언트 또는 consumer 채널을 관찰자와 연결한다.
61
+ * @process WeakMap에 등록하여 전송 객체 수명 이후 참조를 유지하지 않는다.
62
+ */
63
+ export declare function registerRmqObservation(owner: object, observer: RmqConnectionObserver): void;
64
+ /**
65
+ * @author jun
66
+ * @description 현재 연결 상태의 독립적인 스냅샷을 반환한다.
67
+ * @process 관찰 대상이 아니면 undefined를 반환한다.
68
+ */
69
+ export declare function getRmqDiagnosticSnapshot(owner: object): Snapshot | undefined;
70
+ /**
71
+ * @author jun
72
+ * @description 이미 기록한 오류 상세를 표시한다.
73
+ * @process 요청별 호출 정보 없이 오류 메타데이터만 WeakMap에 저장한다.
74
+ */
75
+ export declare function recordRmqDiagnosticError(error: unknown, diagnostic: NormalizedDiagnosticError): void;
76
+ /**
77
+ * @author jun
78
+ * @description 동일 오류의 중복 상세 기록 여부를 확인한다.
79
+ * @process 객체 오류에 연결된 정규화 결과를 반환한다.
80
+ */
81
+ export declare function getRecordedRmqDiagnosticError(error: unknown): NormalizedDiagnosticError | undefined;
82
+ /** Same factory/options and lazy connection behavior; observation is opt-in per process. */
83
+ /**
84
+ * @author jun
85
+ * @description lazy 연결을 유지하는 RMQ 클라이언트를 생성한다.
86
+ * @process 진단이 활성화되면 기존 ClientRMQ를 확장하고 그 외에는 기본 factory를 사용한다.
87
+ */
88
+ export declare function createDiagnosticRmqClient(options: RmqOptions): ClientRMQ;
89
+ export {};
@@ -0,0 +1,300 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.createDiagnosticRmqClient = exports.getRecordedRmqDiagnosticError = exports.recordRmqDiagnosticError = exports.getRmqDiagnosticSnapshot = exports.registerRmqObservation = exports.withRmqConnectionName = exports.createRmqObserver = exports.RmqConnectionObserver = exports.isRmqDiagnosticsEnabled = void 0;
30
+ const node_crypto_1 = require("node:crypto");
31
+ const node_os_1 = __importDefault(require("node:os"));
32
+ const microservices_1 = require("@nestjs/microservices");
33
+ const msa_logger_1 = __importStar(require("@yolo-croket-dev/msa-logger"));
34
+ const observations = new WeakMap();
35
+ const recordedErrors = new WeakMap();
36
+ let logger;
37
+ /**
38
+ * @author jun
39
+ * @description 프로세스의 RMQ 연결 진단 활성 여부를 확인한다.
40
+ * @process 명시적인 true 값만 활성으로 해석한다.
41
+ */
42
+ function isRmqDiagnosticsEnabled() {
43
+ return process.env.RMQ_DIAGNOSTICS_ENABLED === 'true';
44
+ }
45
+ exports.isRmqDiagnosticsEnabled = isRmqDiagnosticsEnabled;
46
+ function writeConnectionEvent(info, details) {
47
+ logger !== null && logger !== void 0 ? logger : (logger = new msa_logger_1.default(msa_logger_1.ServerTypeEnum.SERVICE));
48
+ const method = info.event === 'connected' || info.event === 'unblocked' ? 'info' : 'warn';
49
+ logger[method]('RMQ_CONNECTION', {
50
+ traceId: '-',
51
+ diagnosticInfo: info,
52
+ }, details);
53
+ }
54
+ /**
55
+ * @author jun
56
+ * @description 전송 동작을 바꾸지 않고 연결 상태와 반복 실패를 기록한다.
57
+ * @process 연결 이벤트 구독 → 상태 갱신/60초 실패 요약 → 종료 시 listener/timer 정리.
58
+ */
59
+ class RmqConnectionObserver {
60
+ constructor(queue, direction, configuredName, write = writeConnectionEvent) {
61
+ this.direction = direction;
62
+ this.write = write;
63
+ this.connectionId = (0, node_crypto_1.randomUUID)();
64
+ this.generation = 0;
65
+ this.state = 'not_connected';
66
+ this.changedAt = new Date().toISOString();
67
+ this.listeners = [];
68
+ this.failed = false;
69
+ this.repeatCount = 0;
70
+ this.targetService = (0, msa_logger_1.sanitizeDiagnosticText)(queue || 'unknown').slice(0, 240);
71
+ this.connectionName = (configuredName === null || configuredName === void 0 ? void 0 : configuredName.trim()) ? configuredName
72
+ : `${process.env.SERVER_NAME || 'unknown'}/${direction}/${queue || 'unknown'}/${node_os_1.default.hostname()}/${this.connectionId}`;
73
+ }
74
+ snapshot() {
75
+ return {
76
+ connectionId: this.connectionId,
77
+ connectionName: (0, msa_logger_1.sanitizeDiagnosticText)(this.connectionName).slice(0, 240),
78
+ direction: this.direction,
79
+ targetService: this.targetService,
80
+ generation: this.generation,
81
+ connectionState: this.state,
82
+ stateChangedAt: this.changedAt,
83
+ };
84
+ }
85
+ transition(state) {
86
+ if (this.state !== state) {
87
+ this.state = state;
88
+ this.changedAt = new Date().toISOString();
89
+ }
90
+ }
91
+ emit(event, error, extra = {}) {
92
+ // Diagnostics must not throw into the connection manager's synchronous event dispatch.
93
+ try {
94
+ this.write({
95
+ ...error === null || error === void 0 ? void 0 : error.diagnosticInfo,
96
+ ...this.snapshot(),
97
+ event,
98
+ ...extra,
99
+ }, error === null || error === void 0 ? void 0 : error.details);
100
+ }
101
+ catch { /* The transport remains authoritative if the log sink fails. */ }
102
+ }
103
+ flush() {
104
+ if (this.timer)
105
+ clearTimeout(this.timer);
106
+ this.timer = undefined;
107
+ if (this.repeatCount) {
108
+ this.emit('connect_failed_summary', this.latestFailure, {
109
+ repeatCount: this.repeatCount,
110
+ firstOccurredAt: this.firstOccurredAt,
111
+ lastOccurredAt: this.lastOccurredAt,
112
+ });
113
+ }
114
+ this.repeatCount = 0;
115
+ this.latestFailure = undefined;
116
+ this.firstOccurredAt = undefined;
117
+ this.lastOccurredAt = undefined;
118
+ }
119
+ onFailure(value) {
120
+ var _a;
121
+ this.transition('connection_failed');
122
+ const error = (0, msa_logger_1.normalizeDiagnosticError)(value, 'connection_failed');
123
+ if (!this.failed) {
124
+ this.failed = true;
125
+ this.emit('connect_failed', error);
126
+ return;
127
+ }
128
+ this.repeatCount += 1;
129
+ this.latestFailure = error;
130
+ this.lastOccurredAt = new Date().toISOString();
131
+ (_a = this.firstOccurredAt) !== null && _a !== void 0 ? _a : (this.firstOccurredAt = this.lastOccurredAt);
132
+ if (!this.timer) {
133
+ this.timer = setTimeout(() => this.flush(), 60000);
134
+ this.timer.unref();
135
+ }
136
+ }
137
+ observe(emitter) {
138
+ if (this.emitter === emitter)
139
+ return;
140
+ this.detach();
141
+ this.emitter = emitter;
142
+ this.listeners = [
143
+ [
144
+ 'connect',
145
+ () => {
146
+ this.flush();
147
+ this.failed = false;
148
+ this.generation += 1;
149
+ this.transition('connected');
150
+ this.emit('connected');
151
+ },
152
+ ],
153
+ [
154
+ 'connectFailed',
155
+ value => this.onFailure(value),
156
+ ],
157
+ [
158
+ 'disconnect',
159
+ value => {
160
+ this.transition('disconnected');
161
+ this.emit('disconnected', (0, msa_logger_1.normalizeDiagnosticError)(value, 'connection_lost'));
162
+ },
163
+ ],
164
+ [
165
+ 'blocked',
166
+ () => {
167
+ this.transition('blocked');
168
+ this.emit('blocked');
169
+ },
170
+ ],
171
+ [
172
+ 'unblocked',
173
+ () => {
174
+ this.transition('connected');
175
+ this.emit('unblocked');
176
+ },
177
+ ],
178
+ ];
179
+ this.listeners.forEach(([event, listener,]) => emitter.on(event, listener));
180
+ }
181
+ detach() {
182
+ this.listeners.forEach(([event, listener,]) => { var _a; return (_a = this.emitter) === null || _a === void 0 ? void 0 : _a.removeListener(event, listener); });
183
+ this.listeners = [];
184
+ this.emitter = undefined;
185
+ }
186
+ close() {
187
+ this.flush();
188
+ this.detach();
189
+ this.failed = false;
190
+ this.transition('closed');
191
+ }
192
+ }
193
+ exports.RmqConnectionObserver = RmqConnectionObserver;
194
+ /**
195
+ * @author jun
196
+ * @description 기존 연결 옵션으로 관찰자를 만든다.
197
+ * @process 명시적인 broker 연결 이름을 읽고 인스턴스 식별자를 생성한다.
198
+ */
199
+ function createRmqObserver(options, direction) {
200
+ var _a, _b;
201
+ const properties = (_b = (_a = options.socketOptions) === null || _a === void 0 ? void 0 : _a.connectionOptions) === null || _b === void 0 ? void 0 : _b.clientProperties;
202
+ const configured = properties && typeof properties === 'object'
203
+ ? properties.connection_name : undefined;
204
+ return new RmqConnectionObserver(options.queue, direction, typeof configured === 'string' ? configured : undefined);
205
+ }
206
+ exports.createRmqObserver = createRmqObserver;
207
+ /**
208
+ * @author jun
209
+ * @description 기존 RMQ 옵션에 연결 이름만 추가한다.
210
+ * @process 원본 옵션과 명시적인 연결 이름을 보존한 복사본을 반환한다.
211
+ */
212
+ function withRmqConnectionName(options, observer) {
213
+ var _a, _b, _c;
214
+ return {
215
+ ...options,
216
+ socketOptions: {
217
+ ...options.socketOptions,
218
+ connectionOptions: {
219
+ ...(_a = options.socketOptions) === null || _a === void 0 ? void 0 : _a.connectionOptions,
220
+ clientProperties: {
221
+ ...(_c = (_b = options.socketOptions) === null || _b === void 0 ? void 0 : _b.connectionOptions) === null || _c === void 0 ? void 0 : _c.clientProperties,
222
+ connection_name: observer.connectionName,
223
+ },
224
+ },
225
+ },
226
+ };
227
+ }
228
+ exports.withRmqConnectionName = withRmqConnectionName;
229
+ /**
230
+ * @author jun
231
+ * @description 클라이언트 또는 consumer 채널을 관찰자와 연결한다.
232
+ * @process WeakMap에 등록하여 전송 객체 수명 이후 참조를 유지하지 않는다.
233
+ */
234
+ function registerRmqObservation(owner, observer) {
235
+ observations.set(owner, observer);
236
+ }
237
+ exports.registerRmqObservation = registerRmqObservation;
238
+ /**
239
+ * @author jun
240
+ * @description 현재 연결 상태의 독립적인 스냅샷을 반환한다.
241
+ * @process 관찰 대상이 아니면 undefined를 반환한다.
242
+ */
243
+ function getRmqDiagnosticSnapshot(owner) {
244
+ var _a;
245
+ return (_a = observations.get(owner)) === null || _a === void 0 ? void 0 : _a.snapshot();
246
+ }
247
+ exports.getRmqDiagnosticSnapshot = getRmqDiagnosticSnapshot;
248
+ // Contains error metadata only, never per-request context: one RMQ error can reject concurrent calls.
249
+ /**
250
+ * @author jun
251
+ * @description 이미 기록한 오류 상세를 표시한다.
252
+ * @process 요청별 호출 정보 없이 오류 메타데이터만 WeakMap에 저장한다.
253
+ */
254
+ function recordRmqDiagnosticError(error, diagnostic) {
255
+ if (error !== null && typeof error === 'object')
256
+ recordedErrors.set(error, diagnostic);
257
+ }
258
+ exports.recordRmqDiagnosticError = recordRmqDiagnosticError;
259
+ /**
260
+ * @author jun
261
+ * @description 동일 오류의 중복 상세 기록 여부를 확인한다.
262
+ * @process 객체 오류에 연결된 정규화 결과를 반환한다.
263
+ */
264
+ function getRecordedRmqDiagnosticError(error) {
265
+ return error !== null && typeof error === 'object' ? recordedErrors.get(error) : undefined;
266
+ }
267
+ exports.getRecordedRmqDiagnosticError = getRecordedRmqDiagnosticError;
268
+ class DiagnosticClientRMQ extends microservices_1.ClientRMQ {
269
+ constructor(options, observation) {
270
+ super(withRmqConnectionName(options, observation));
271
+ this.observation = observation;
272
+ registerRmqObservation(this, observation);
273
+ }
274
+ createClient() {
275
+ const client = super.createClient();
276
+ this.observation.observe(client);
277
+ return client;
278
+ }
279
+ async close() {
280
+ try {
281
+ await super.close();
282
+ }
283
+ finally {
284
+ this.observation.close();
285
+ }
286
+ }
287
+ }
288
+ /** Same factory/options and lazy connection behavior; observation is opt-in per process. */
289
+ /**
290
+ * @author jun
291
+ * @description lazy 연결을 유지하는 RMQ 클라이언트를 생성한다.
292
+ * @process 진단이 활성화되면 기존 ClientRMQ를 확장하고 그 외에는 기본 factory를 사용한다.
293
+ */
294
+ function createDiagnosticRmqClient(options) {
295
+ if (!isRmqDiagnosticsEnabled())
296
+ return microservices_1.ClientProxyFactory.create(options);
297
+ const connectionOptions = options.options || {};
298
+ return new DiagnosticClientRMQ(connectionOptions, createRmqObserver(connectionOptions, 'client'));
299
+ }
300
+ exports.createDiagnosticRmqClient = createDiagnosticRmqClient;