@rsdk/core 4.0.0-next.9 → 4.0.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 (136) hide show
  1. package/CHANGELOG.md +130 -40
  2. package/dist/app-metadata/app-name.validator.js.map +1 -1
  3. package/dist/config/additional-source/additional-source.initializer.js.map +1 -1
  4. package/dist/config/config-reload.indicator.js.map +1 -1
  5. package/dist/config/config.abstract.js.map +1 -1
  6. package/dist/config/config.module.js.map +1 -1
  7. package/dist/config/context/config.context.js.map +1 -1
  8. package/dist/config/metadata/config-metadata.registry.js.map +1 -1
  9. package/dist/config/metadata/decorators/config-section.decorator.js.map +1 -1
  10. package/dist/config/parsers/array.parser.js.map +1 -1
  11. package/dist/config/parsers/path/fspath.parser.js.map +1 -1
  12. package/dist/config/parsers/url/url.parser.js.map +1 -1
  13. package/dist/config/sources/base/config-source.abstract.js.map +1 -1
  14. package/dist/config/sources/base/reloadable-config-source.abstract.js.map +1 -1
  15. package/dist/config/sources/implementations/relodable-json-file.source.js.map +1 -1
  16. package/dist/config/strategy/app-name-strategy.validator.js.map +1 -1
  17. package/dist/config/types.js.map +1 -1
  18. package/dist/config/vars.class.js.map +1 -1
  19. package/dist/context.aggregator.js.map +1 -1
  20. package/dist/exceptions/base/platform-exception.absract.js.map +1 -1
  21. package/dist/exceptions/implementations/bootstrap/double-init.exception.js.map +1 -1
  22. package/dist/exceptions/implementations/bootstrap/no-init.exception.js.map +1 -1
  23. package/dist/exceptions/metadata/exceptions.registry.js.map +1 -1
  24. package/dist/exceptions.handling/global-exceptions.filter.d.ts +3 -1
  25. package/dist/exceptions.handling/global-exceptions.filter.js +10 -4
  26. package/dist/exceptions.handling/global-exceptions.filter.js.map +1 -1
  27. package/dist/exceptions.handling/types.d.ts +4 -11
  28. package/dist/health/autodoc/heath.autodoc-resolver.js.map +1 -1
  29. package/dist/health/health.service.js.map +1 -1
  30. package/dist/health/indicators.abstract/fs-access.indicator.js.map +1 -1
  31. package/dist/index.d.ts +7 -1
  32. package/dist/index.js +22 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/logging/logging.config.js +1 -2
  35. package/dist/logging/logging.config.js.map +1 -1
  36. package/dist/metrics/metadata/autodoc/metrics.autodoc-resolver.js.map +1 -1
  37. package/dist/metrics/metrics.module.js.map +1 -1
  38. package/dist/platform.context.js.map +1 -1
  39. package/dist/platform.module.js +0 -1
  40. package/dist/platform.module.js.map +1 -1
  41. package/dist/plugin/plugin.module.js.map +1 -1
  42. package/dist/rsdk-metadata/config-metadata.extractor.js.map +1 -1
  43. package/dist/tracing/active-span.module.d.ts +5 -0
  44. package/dist/tracing/active-span.module.js +25 -0
  45. package/dist/tracing/active-span.module.js.map +1 -0
  46. package/dist/tracing/constants.d.ts +7 -1
  47. package/dist/tracing/constants.js +8 -2
  48. package/dist/tracing/constants.js.map +1 -1
  49. package/dist/tracing/decorators/span.decorator.d.ts +2 -2
  50. package/dist/tracing/decorators/span.decorator.js +14 -2
  51. package/dist/tracing/decorators/span.decorator.js.map +1 -1
  52. package/dist/tracing/index.d.ts +7 -0
  53. package/dist/tracing/index.js +7 -0
  54. package/dist/tracing/index.js.map +1 -1
  55. package/dist/tracing/request-metadata.module.d.ts +5 -0
  56. package/dist/tracing/request-metadata.module.js +25 -0
  57. package/dist/tracing/request-metadata.module.js.map +1 -0
  58. package/dist/tracing/services/active-span.storage.d.ts +17 -0
  59. package/dist/tracing/services/active-span.storage.js +46 -0
  60. package/dist/tracing/services/active-span.storage.js.map +1 -0
  61. package/dist/tracing/services/index.d.ts +0 -1
  62. package/dist/tracing/services/index.js +0 -1
  63. package/dist/tracing/services/index.js.map +1 -1
  64. package/dist/tracing/services/instrumentation.service.d.ts +2 -8
  65. package/dist/tracing/services/instrumentation.service.js +9 -114
  66. package/dist/tracing/services/instrumentation.service.js.map +1 -1
  67. package/dist/tracing/services/request-metadata.injector.d.ts +6 -0
  68. package/dist/tracing/services/request-metadata.injector.js +122 -0
  69. package/dist/tracing/services/request-metadata.injector.js.map +1 -0
  70. package/dist/tracing/services/request-metadata.storage.d.ts +32 -0
  71. package/dist/tracing/services/request-metadata.storage.js +64 -0
  72. package/dist/tracing/services/request-metadata.storage.js.map +1 -0
  73. package/dist/tracing/services/trace.injector.d.ts +1 -1
  74. package/dist/tracing/services/trace.injector.js +213 -18
  75. package/dist/tracing/services/trace.injector.js.map +1 -1
  76. package/dist/tracing/tracing.interceptor.d.ts +9 -0
  77. package/dist/tracing/tracing.interceptor.js +24 -0
  78. package/dist/tracing/tracing.interceptor.js.map +1 -0
  79. package/dist/tracing/tracing.module.d.ts +1 -1
  80. package/dist/tracing/tracing.module.js +15 -16
  81. package/dist/tracing/tracing.module.js.map +1 -1
  82. package/dist/tracing/utils/create-span.d.ts +10 -0
  83. package/dist/tracing/utils/create-span.js +20 -0
  84. package/dist/tracing/utils/create-span.js.map +1 -0
  85. package/dist/tracing/utils/save-async-hooks-context.d.ts +19 -0
  86. package/dist/tracing/utils/save-async-hooks-context.js +57 -0
  87. package/dist/tracing/utils/save-async-hooks-context.js.map +1 -0
  88. package/dist/transport/get-transport-id.d.ts +5 -0
  89. package/dist/transport/get-transport-id.js +14 -0
  90. package/dist/transport/get-transport-id.js.map +1 -0
  91. package/dist/transport/protocol.detector.d.ts +7 -0
  92. package/dist/transport/protocol.detector.js +41 -0
  93. package/dist/transport/protocol.detector.js.map +1 -0
  94. package/dist/transport/transport.module.js +9 -0
  95. package/dist/transport/transport.module.js.map +1 -1
  96. package/dist/types/index.d.ts +1 -0
  97. package/dist/types/index.js +4 -0
  98. package/dist/types/index.js.map +1 -1
  99. package/dist/types/metadata.js.map +1 -1
  100. package/dist/types/transports.d.ts +12 -0
  101. package/dist/types/transports.js.map +1 -1
  102. package/dist/unhandled-rejection.handler.js.map +1 -1
  103. package/package.json +12 -12
  104. package/src/config/reload/config-reload.events.ts +9 -0
  105. package/src/config/sources/base/config-source.abstract.ts +2 -0
  106. package/src/config/types.ts +3 -0
  107. package/src/exceptions.handling/global-exceptions.filter.ts +6 -2
  108. package/src/exceptions.handling/types.ts +4 -11
  109. package/src/index.ts +25 -1
  110. package/src/logging/logging.config.ts +1 -2
  111. package/src/platform.module.ts +1 -2
  112. package/src/tracing/active-span.module.ts +13 -0
  113. package/src/tracing/constants.ts +8 -1
  114. package/src/tracing/decorators/span.decorator.ts +19 -6
  115. package/src/tracing/index.ts +7 -0
  116. package/src/tracing/request-metadata.module.ts +13 -0
  117. package/src/tracing/services/active-span.storage.ts +32 -0
  118. package/src/tracing/services/index.ts +0 -1
  119. package/src/tracing/services/instrumentation.service.ts +16 -130
  120. package/src/tracing/services/request-metadata.injector.ts +153 -0
  121. package/src/tracing/services/request-metadata.storage.ts +69 -0
  122. package/src/tracing/services/trace.injector.ts +268 -19
  123. package/src/tracing/tracing.interceptor.ts +18 -0
  124. package/src/tracing/tracing.module.ts +14 -14
  125. package/src/tracing/utils/create-span.ts +20 -0
  126. package/src/tracing/utils/save-async-hooks-context.ts +61 -0
  127. package/src/transport/get-transport-id.ts +20 -0
  128. package/src/transport/protocol.detector.ts +38 -0
  129. package/src/transport/transport.module.ts +10 -0
  130. package/src/types/index.ts +2 -0
  131. package/src/types/transports.ts +15 -0
  132. package/tsconfig.json +12 -2
  133. package/dist/tracing/services/metadata.scanner.d.ts +0 -11
  134. package/dist/tracing/services/metadata.scanner.js +0 -50
  135. package/dist/tracing/services/metadata.scanner.js.map +0 -1
  136. package/src/tracing/services/metadata.scanner.ts +0 -40
@@ -1,10 +1,19 @@
1
+ import type { ExecutionContext } from '@nestjs/common';
1
2
  import type { AttributeValue, Span } from '@opentelemetry/api';
2
3
  import { SpanStatusCode, trace } from '@opentelemetry/api';
4
+ import {
5
+ X_B3_PARENT_SPAN_ID,
6
+ X_B3_SPAN_ID,
7
+ X_B3_TRACE_ID,
8
+ } from '@opentelemetry/propagator-b3';
9
+ import { api } from '@opentelemetry/sdk-node';
10
+ import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
3
11
  import type { ErrorLike } from '@rsdk/common';
4
12
  import { isPrimitive, isRecord, normalizer } from '@rsdk/common';
5
13
  import { redecorate } from '@rsdk/decorators';
6
14
  import { LoggerFactory } from '@rsdk/logging';
7
15
  import assert from 'node:assert';
16
+ import { catchError, mergeMap, Observable, of, throwError } from 'rxjs';
8
17
 
9
18
  import { Constants, MAX_BYTES } from '../constants';
10
19
 
@@ -17,6 +26,7 @@ export class TraceInjector {
17
26
  // eslint-disable-next-line @typescript-eslint/ban-types
18
27
  original: Function,
19
28
  descriptor?: TypedPropertyDescriptor<any>,
29
+ isTracingInterceptor?: boolean,
20
30
  ): void {
21
31
  /**
22
32
  * Означает, что данный метод уже обёрнут
@@ -27,8 +37,15 @@ export class TraceInjector {
27
37
 
28
38
  logger.trace(`Wrapping method: ${cls.constructor.name}.${original.name}()`);
29
39
 
30
- const spanName = TraceInjector.createSpanName(cls, original.name);
31
- const wrapped = TraceInjector.createWrapper(original, spanName);
40
+ const spanName = TraceInjector.createSpanName(
41
+ cls.constructor.name,
42
+ original.name,
43
+ );
44
+ const wrapped = TraceInjector.createWrapper(
45
+ original,
46
+ spanName,
47
+ isTracingInterceptor,
48
+ );
32
49
 
33
50
  redecorate(original, wrapped);
34
51
 
@@ -54,46 +71,275 @@ export class TraceInjector {
54
71
  if (isRecord(data) || Array.isArray(data)) {
55
72
  const serialized = JSON.stringify(normalizer()(data));
56
73
 
57
- return serialized.length <= MAX_BYTES
74
+ return serialized.length <= MAX_BYTES.bytes()
58
75
  ? serialized
59
- : `data exceeds limit of ${MAX_BYTES} bytes`;
76
+ : `data exceeds limit of ${MAX_BYTES.bytes()} bytes`;
60
77
  }
61
78
 
62
79
  return 'data is undefined';
63
80
  }
64
81
 
65
- private static createWrapper(original: any, spanName: string): any {
82
+ private static createWrapper(
83
+ original: any,
84
+ spanName: string,
85
+ isTracingInterceptor?: boolean,
86
+ ): any {
66
87
  return {
67
88
  [original.name](...args: any[]): any {
68
89
  const tracer = trace.getTracer('@rsdk/open-telemetry', '1.0.0');
90
+ const executionContext: ExecutionContext = args[0];
69
91
 
70
- return tracer.startActiveSpan(spanName, (span) => {
92
+ /**
93
+ * Переменный для хранения значений которые получаем для опен телеметрии
94
+ */
95
+ let traceId: string | undefined;
96
+ let spanId: string | undefined;
97
+ let parentSpanId: string | undefined;
98
+ /**
99
+ * Этот флаг нужен для детекта первого входа и старта трейсинга, логика использования ниже в контексте
100
+ */
101
+ let hasParentSpanId = false;
102
+
103
+ /**
104
+ * Переменная для хранения Request/виртуального рекRequestвеста
105
+ */
106
+ let req: any;
107
+
108
+ /**
109
+ * Первый вход обычно происходит в глобальном интерцепторе
110
+ * он оборачивается в некий свой интернал тип спана который и передает этот флаг isTracingInterceptor=true
111
+ */
112
+ if (isTracingInterceptor) {
113
+ if (executionContext.getType() === 'rpc') {
114
+ const metadata = executionContext.switchToRpc().getContext();
115
+ /**
116
+ * Если мы пришли в GRPC и у нас есть методы для работы с Metadata
117
+ * то мы конвертируем Metadata в некий виртуальный request с заголовками (заголовки выбраны как общий стандарт проброса мета информации)
118
+ */
119
+ if (metadata?.get) {
120
+ req = {
121
+ headers: {
122
+ /**
123
+ * Пробрасываем текущий трейс ид запроса
124
+ */
125
+ ...(metadata.get(X_B3_TRACE_ID).length > 0
126
+ ? {
127
+ [X_B3_TRACE_ID]: metadata.get(X_B3_TRACE_ID)[0],
128
+ }
129
+ : {}),
130
+ /**
131
+ * Пробрасываем текущий спан ид, он нужен чтобы установить цепочку с парентами и могли видеть правильную вложенность в графане
132
+ */
133
+ ...(metadata.get(X_B3_SPAN_ID).length > 0
134
+ ? {
135
+ [X_B3_SPAN_ID]: metadata.get(X_B3_SPAN_ID)[0],
136
+ }
137
+ : {}),
138
+ /**
139
+ * Вот это не обычная переменная, она нужна для определения общего входа и детекта рутового спан ид,
140
+ * эта переменная нужна для работы флага hasParentSpanId
141
+ */
142
+ ...(metadata.get(X_B3_PARENT_SPAN_ID).length > 0
143
+ ? {
144
+ [X_B3_PARENT_SPAN_ID]:
145
+ metadata.get(X_B3_PARENT_SPAN_ID)[0],
146
+ }
147
+ : {}),
148
+ },
149
+ };
150
+ }
151
+ }
152
+
153
+ if (executionContext.getType() === 'http') {
154
+ req = executionContext.switchToHttp().getRequest();
155
+ }
156
+
157
+ if (executionContext.getType<string>() === 'graphql') {
158
+ req = executionContext.getArgs()[2]?.req; // аналог GqlExecutionContext.create(executionContext).getContext().req
159
+
160
+ if (req.connectionInitReceived) {
161
+ /**
162
+ * При работе с сабскрипшен через веб сокет, заголовки передаются в опции подключения к сабскрипшен в переменную connectionParams
163
+ * и мы перегоняем эти данные в request заголовки
164
+ */
165
+ req.headers = {
166
+ ...req?.headers,
167
+ ...Object.entries(req?.connectionParams?.headers || {}).reduce(
168
+ (acc, [key, value]) => {
169
+ acc[key] = value;
170
+ return acc;
171
+ },
172
+ {},
173
+ ),
174
+ };
175
+ }
176
+ }
177
+ }
178
+
179
+ if (req?.headers) {
180
+ /**
181
+ * Мы приводим все ключи заголовков к одному **lower_case** регистру, так как у разных транспортов разные регистры в именовании ключей для заголовков
182
+ */
183
+ req.headers = {
184
+ ...Object.entries(req.headers).reduce((acc, [key, value]) => {
185
+ acc[key.toLowerCase()] = req.headers[key.toLowerCase()] || value;
186
+ return acc;
187
+ }, {}),
188
+ };
189
+
190
+ /**
191
+ * Трейс ид - входящий рутовый идентификатор, он по всем цепочкам идет
192
+ */
193
+ traceId = req.headers[X_B3_TRACE_ID];
194
+ /**
195
+ * Спан ид - идентификатор который нужен для построения цепочки вызовов
196
+ */
197
+ spanId = req.headers[X_B3_SPAN_ID];
198
+ /**
199
+ * Флаг определяет находимся ли мы на рутовой позиции (самая первая точка входа)
200
+ * определяется просто - если с клиента не передали заголовок X_B3_PARENT_SPAN_ID то значит мы в точке входа,
201
+ * иначе мы являемся частью другой общей цепочки
202
+ */
203
+ hasParentSpanId = !!Object.getOwnPropertyDescriptor(
204
+ req.headers,
205
+ X_B3_PARENT_SPAN_ID,
206
+ );
207
+ /**
208
+ * Идентификатор родительского спана - больше нужен для определения рутового состояния в различных кодах по опен телеметрии
209
+ */
210
+ parentSpanId = req.headers[X_B3_PARENT_SPAN_ID];
211
+ }
212
+
213
+ if (isTracingInterceptor) {
214
+ /**
215
+ * Когда мы находимся в интерцепторе, то названием спана является название класса + метод интерцептора
216
+ * мы перебиваем на название класса + метод который трекаем
217
+ */
218
+ spanName = TraceInjector.createSpanName(
219
+ executionContext.getClass().name,
220
+ executionContext.getHandler().name,
221
+ );
222
+ }
223
+
224
+ /**
225
+ * Создаем новый спан
226
+ */
227
+ const span = tracer.startSpan(spanName, {});
228
+
229
+ // path parentSpanId for correct link to parent span
230
+ if (isTracingInterceptor && hasParentSpanId && parentSpanId) {
231
+ /**
232
+ * Так как опентелеметрия ставит свой некий парент спан ид, то наш слетает
233
+ * если мы работаем в рамках монолита то ничего не слетает
234
+ * как только мы перемещаемся между приложениями - он слетает
235
+ * чтобы иметь корректный парент спан ид - мы патчим его жестко (из коробки нельзя это сделать)
236
+ */
237
+ (span as any).parentSpanId = parentSpanId;
238
+ }
239
+
240
+ /**
241
+ * После создания контекста ранее при входе в приложение, у нас новый спан ид
242
+ * мы перетираем значением которе получили через заголовок
243
+ * в рамках приложения он корректный, но при переходе из одного приложения в другой - слетает
244
+ * и чтобы 100% все было норм, мы всегда патчим его
245
+ */
246
+ if (spanId) {
247
+ span.spanContext().spanId = spanId;
248
+ }
249
+
250
+ /**
251
+ * Патчим трайс ид, причина выше
252
+ */
253
+ if (traceId) {
254
+ span.spanContext().traceId = traceId;
255
+ }
256
+
257
+ if (req?.headers) {
258
+ /**
259
+ * Обычно самая первая точка входа это фронтовый запрос, который содержит некие заголовки хттп
260
+ * мы их кладем в рутовый спан
261
+ */
262
+ span.setAttribute(SemanticAttributes.HTTP_METHOD, req.method);
263
+ span.setAttribute(
264
+ SemanticAttributes.HTTP_URL,
265
+ req.originalUrl || req.url,
266
+ );
267
+ span.setAttribute(
268
+ SemanticAttributes.HTTP_ROUTE,
269
+ req.route?.path || req.routeOptions?.url || req.routerPath,
270
+ );
271
+ }
272
+
273
+ /**
274
+ * Чтобы пробросить пропатченный спан нужно запустить две строчки ниже:
275
+ * 1) создаем контекст в котором активный спан перебиваем новым
276
+ * 2) созданный контекст ставим основным и в нем запускаем под процесс
277
+ */
278
+ const spanContext = api.trace.setSpan(api.context.active(), span);
279
+
280
+ return api.context.with(spanContext, async () => {
71
281
  try {
72
- logger.trace(`Wrapped function invoked: ${original.name}`);
282
+ /**
283
+ * При запуске функции обернутой в опентелеметрию мы выводим и трейсИд и спанИд
284
+ * чтобы в ручную по логам отсмотреть порядок запусков, в случаи если нет доступа до борды графаны и на руках есть только файл с логами
285
+ */
286
+ logger.trace(
287
+ `Wrapped function invoked: ${original.name}, traceId: ${
288
+ span.spanContext().traceId
289
+ }, spanId: ${span.spanContext().spanId}`,
290
+ );
73
291
 
74
292
  const result = original.apply(this, args);
75
293
 
76
294
  /**
77
- * If function returns promise:
78
- * - errors won't be caught with try-catch
79
- * - enriching span should be don when promise is resolved
80
- *
81
- * That's why this extra branch exists
295
+ * Если метод асинхронный - обрабтываем его и фиксируем успешное или не успешное завершение метода и зарываем спан,
296
+ * чтобы зафиксировать время запроса и видеть его в графане
82
297
  */
83
298
  if (result instanceof Promise) {
84
299
  return result
85
- .then((result: unknown) => TraceInjector.enrich(span, result))
86
- .catch((error: unknown) =>
87
- TraceInjector.recordAndRethrow(error, span),
88
- )
89
- .finally(() => span.end());
300
+ .then(async (result) => {
301
+ TraceInjector.enrich(span, result);
302
+ span.end();
303
+ return result;
304
+ })
305
+ .catch(async (error) => {
306
+ TraceInjector.recordAndRethrow(error, span);
307
+ span.end();
308
+ throw error;
309
+ });
90
310
  }
91
311
 
312
+ /**
313
+ * Если метод обзервабл - обрабтываем его и фиксируем успешное или не успешное завершение метода и зарываем спан,
314
+ * чтобы зафиксировать время запроса и видеть его в графане
315
+ */
316
+ if (result instanceof Observable) {
317
+ return result.pipe(
318
+ mergeMap((result) => {
319
+ TraceInjector.enrich(span, result);
320
+ span.end();
321
+ return of(result);
322
+ }),
323
+ catchError((error) => {
324
+ TraceInjector.recordAndRethrow(error, span);
325
+ span.end();
326
+ return throwError(() => error);
327
+ }),
328
+ );
329
+ }
330
+
331
+ /**
332
+ * Если метод вернул статичное значение - фиксируем успешное или не успешное завершение метода и зарываем спан,
333
+ * чтобы зафиксировать время запроса и видеть его в графане
334
+ */
92
335
  TraceInjector.enrich(span, result);
93
336
  span.end();
94
337
 
95
338
  return result;
96
339
  } catch (error) {
340
+ /**
341
+ * Обрабатываем ошибку и закрываем спан при попытке получения статичного результата или любую иную ошибку
342
+ */
97
343
  TraceInjector.recordAndRethrow(error, span);
98
344
  span.end();
99
345
  }
@@ -102,8 +348,11 @@ export class TraceInjector {
102
348
  }[original.name];
103
349
  }
104
350
 
105
- private static createSpanName(cls: object, methodName: string): string {
106
- return `${cls.constructor.name} -> ${methodName}`;
351
+ private static createSpanName(className: string, methodName: string): string {
352
+ /**
353
+ * Всего лишь правила формирования имени спана (не более того), для новой логики нужен был именно такой код
354
+ */
355
+ return `${className} -> ${methodName}`;
107
356
  }
108
357
 
109
358
  private static isWrapped(prototype: object): boolean {
@@ -0,0 +1,18 @@
1
+ import type {
2
+ CallHandler,
3
+ ExecutionContext,
4
+ NestInterceptor,
5
+ } from '@nestjs/common';
6
+ import { Injectable } from '@nestjs/common';
7
+ import type { Observable } from 'rxjs';
8
+
9
+ /**
10
+ * Интернал глобальный интерцептор для работы опентелеметрии
11
+ * его поведение будет модифицировано в зависимости от включенного или выключенного состояния глобального трэйсинга
12
+ */
13
+ @Injectable()
14
+ export class TracingInterceptor implements NestInterceptor {
15
+ intercept(_context: ExecutionContext, next: CallHandler): Observable<any> {
16
+ return next.handle();
17
+ }
18
+ }
@@ -1,10 +1,9 @@
1
1
  import type { DynamicModule, Provider } from '@nestjs/common';
2
2
  import { Module } from '@nestjs/common';
3
+ import { APP_INTERCEPTOR } from '@nestjs/core';
3
4
  import { context, trace } from '@opentelemetry/api';
4
5
  import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
5
- import { CompositePropagator } from '@opentelemetry/core';
6
6
  import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
7
- import { B3InjectEncoding, B3Propagator } from '@opentelemetry/propagator-b3';
8
7
  import { Resource } from '@opentelemetry/resources';
9
8
  import { NodeSDK } from '@opentelemetry/sdk-node';
10
9
  import type { SpanExporter } from '@opentelemetry/sdk-trace-base';
@@ -17,9 +16,11 @@ import { ILogger, LoggerFactory } from '@rsdk/logging';
17
16
 
18
17
  import { InjectLogger } from '../logging';
19
18
 
20
- import { InstrumentationService } from './services/instrumentation.service';
21
- import { ExtendedMetadataScanner } from './services/metadata.scanner';
19
+ import { ActiveSpanModule } from './active-span.module';
20
+ import { RequestMetadataModule } from './request-metadata.module';
21
+ import { InstrumentationService } from './services';
22
22
  import { TracingModuleConfig } from './tracing.config';
23
+ import { TracingInterceptor } from './tracing.interceptor';
23
24
 
24
25
  export interface TracingModuleOptions {
25
26
  appName: string;
@@ -39,10 +40,14 @@ export class TracingModule {
39
40
  static forRoot(options: TracingModuleOptions): DynamicModule {
40
41
  return {
41
42
  module: TracingModule,
43
+ imports: [RequestMetadataModule, ActiveSpanModule],
42
44
  providers: [
43
- ExtendedMetadataScanner,
44
45
  InstrumentationService,
45
46
  this.createSDKProvider(options),
47
+ /**
48
+ * Глобальный интерцептор для проброса входящих traceId, spanId и requestId из заголовков и метадаты в активный AsyncLocalStorage запроса
49
+ */
50
+ { provide: APP_INTERCEPTOR, useClass: TracingInterceptor },
46
51
  ],
47
52
  };
48
53
  }
@@ -71,14 +76,6 @@ export class TracingModule {
71
76
  [SemanticResourceAttributes.SERVICE_NAME]: options.appName,
72
77
  }),
73
78
  spanProcessor: processor,
74
- textMapPropagator: new CompositePropagator({
75
- propagators: [
76
- new B3Propagator(),
77
- new B3Propagator({
78
- injectEncoding: B3InjectEncoding.MULTI_HEADER,
79
- }),
80
- ],
81
- }),
82
79
  });
83
80
  },
84
81
  };
@@ -86,6 +83,9 @@ export class TracingModule {
86
83
 
87
84
  // Можно добавить включение и выключение при изменении конфига.
88
85
  async onModuleInit(): Promise<void> {
86
+ this.logger.debug('Injecting request-metadata storage...');
87
+ this.instrumentations.injectWrapRequestMetadataInjector();
88
+
89
89
  if (!this.config.enabled) {
90
90
  this.logger.info('Tracing is disabled');
91
91
 
@@ -98,7 +98,7 @@ export class TracingModule {
98
98
  await this.sdk.start();
99
99
 
100
100
  this.logger.debug('Instrumenting nest.js entities...');
101
- this.instrumentations.inject();
101
+ this.instrumentations.injectWrapTraceInjector();
102
102
 
103
103
  this.logger.debug('Attaching log messages to spans...');
104
104
  LoggerFactory.onMessage((level, msg) => {
@@ -0,0 +1,20 @@
1
+ import { getRandomBytes } from '@rsdk/common';
2
+
3
+ /**
4
+ * Функция для генерации корректного рандомного traceId и spanId
5
+ * алгоритм взят из: https://github.com/openzipkin/zipkin-js/blob/ec89188cf6a07e184ab886c1dfb6c9dc276ddfa4/packages/zipkin/src/tracer/randomTraceId.js
6
+ * спецификация по traceId: https://www.w3.org/TR/trace-context/#considerations-for-trace-id-field-generation
7
+ * @returns {traceId, spanId}
8
+ */
9
+ export function createSpan(): {
10
+ traceId: string;
11
+ spanId: string;
12
+ } {
13
+ const rootSpanId = getRandomBytes(16);
14
+ const traceId = getRandomBytes(16) + rootSpanId;
15
+
16
+ return {
17
+ traceId,
18
+ spanId: rootSpanId,
19
+ };
20
+ }
@@ -0,0 +1,61 @@
1
+ import { SpanStatusCode } from '@opentelemetry/api';
2
+ import { api } from '@opentelemetry/sdk-node';
3
+ import type { ErrorLike } from '@rsdk/common';
4
+
5
+ import { TraceInjector } from '../services';
6
+ import { ActiveSpanStorage } from '../services/active-span.storage';
7
+
8
+ /**
9
+ * Проблема: при передаче управления в rxjs теряется контекст асинк локал стораджа
10
+ *
11
+ * Решение: Утилита для проброса инфы из асинк локал стораджа в тело пайпа rxjs
12
+ * поиском "observableToAsyncIterable" можно найти тесты в которых есть пример использования
13
+ *
14
+ * Тут есть часть логик трейсер декоратора, суть простая:
15
+ * мы - находясь в контексте с актуальным асинк Metadata запускаем создание боди для пайпа rx и тем самым мы передаем контекст работы туда
16
+ * ```
17
+ * return this.booksStream.pipe(
18
+ * concatMap(
19
+ * saveAsyncHooksContext(async (book) => {
20
+ * return { book: { ...book, pages: request.limit } };
21
+ * }),
22
+ * ),
23
+ * );
24
+ * ```
25
+ */
26
+ export function saveAsyncHooksContext<T>(
27
+ fn: (...args: any[]) => Promise<T>,
28
+ ): (...args: any[]) => Promise<T> {
29
+ const span = ActiveSpanStorage.getInstance()?.getActiveSpan();
30
+
31
+ return async function (...args): Promise<T> {
32
+ const activeSpan = ActiveSpanStorage.getInstance()?.getActiveSpan();
33
+ if (activeSpan) {
34
+ Object.assign(activeSpan.spanContext(), span?.spanContext());
35
+ return fn(...args);
36
+ }
37
+ if (!span) {
38
+ return fn(...args);
39
+ }
40
+ const spanContext = api.trace.setSpan(api.context.active(), span);
41
+
42
+ return api.context.with(spanContext, async () =>
43
+ fn(...args)
44
+ .then(async (result) => {
45
+ span.setAttribute('response', TraceInjector.toAttribute(result));
46
+ span.setStatus({ code: SpanStatusCode.OK });
47
+ span.end();
48
+ return result;
49
+ })
50
+ .catch(async (error) => {
51
+ span.recordException(error as ErrorLike);
52
+ span.setStatus({
53
+ code: SpanStatusCode.ERROR,
54
+ message: (error as ErrorLike).message,
55
+ });
56
+ span.end();
57
+ throw error;
58
+ }),
59
+ );
60
+ };
61
+ }
@@ -0,0 +1,20 @@
1
+ import type { ExecutionContext } from '@nestjs/common';
2
+ import { createParamDecorator } from '@nestjs/common';
3
+ import type { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
4
+ import type { Transport } from '@nestjs/microservices';
5
+ import { TRANSPORT_METADATA } from '@nestjs/microservices/constants';
6
+
7
+ export const TransportId = createParamDecorator(
8
+ (_data: unknown, ctx: ExecutionContext | ExecutionContextHost) => {
9
+ return getTransportId(ctx);
10
+ },
11
+ );
12
+
13
+ export function getTransportId(
14
+ context: ExecutionContext | ExecutionContextHost,
15
+ ): Transport {
16
+ return (
17
+ Reflect.getMetadata(TRANSPORT_METADATA, context.getHandler()) ??
18
+ Reflect.getMetadata(TRANSPORT_METADATA, context.getClass())
19
+ );
20
+ }
@@ -0,0 +1,38 @@
1
+ import type { ArgumentsHost } from '@nestjs/common';
2
+
3
+ import { InternalException } from '../exceptions';
4
+ import type { ITransport } from '../types';
5
+
6
+ export class ProtocolDetector {
7
+ constructor(private transports: ITransport[]) {}
8
+
9
+ getProtocol(context: ArgumentsHost): string | undefined {
10
+ if (this.transports.length === 0) {
11
+ return;
12
+ }
13
+ const matchers = this.transports.map((tr) => ({
14
+ matched: tr.matchByContext(context),
15
+ tr,
16
+ }));
17
+ const matched = matchers.filter((matched) => matched.matched);
18
+ if (matched.length > 1) {
19
+ throw new InternalException('So many matchers for transports', {
20
+ cause: {
21
+ matchers,
22
+ matched,
23
+ context,
24
+ },
25
+ });
26
+ }
27
+ if (matched.length === 0) {
28
+ throw new InternalException('No matchers found', {
29
+ cause: {
30
+ matchers,
31
+ matched,
32
+ context,
33
+ },
34
+ });
35
+ }
36
+ return matched[0].tr.getProtocol();
37
+ }
38
+ }
@@ -9,6 +9,8 @@ import { MetricsModule } from '../metrics';
9
9
  import type { PlatformExtendedOptions } from '../types';
10
10
  import { isPrimaryTransport } from '../types';
11
11
 
12
+ import { ProtocolDetector } from './protocol.detector';
13
+
12
14
  export class PlatformTransportModule {
13
15
  static forOptions(options: PlatformExtendedOptions): DynamicModule {
14
16
  return {
@@ -19,6 +21,14 @@ export class PlatformTransportModule {
19
21
  ...PlatformTransportModule.getTransportModules(options),
20
22
  ],
21
23
  module: PlatformTransportModule,
24
+ providers: [
25
+ {
26
+ provide: ProtocolDetector,
27
+ useFactory: () => new ProtocolDetector(options.transports ?? []),
28
+ },
29
+ ],
30
+ global: true,
31
+ exports: [ProtocolDetector],
22
32
  };
23
33
  }
24
34
 
@@ -1,3 +1,5 @@
1
+ export { getTransportId, TransportId } from '../transport/get-transport-id';
2
+
1
3
  export * from './metadata';
2
4
  export * from './options';
3
5
  export * from './transports';
@@ -1,3 +1,4 @@
1
+ import type { ArgumentsHost } from '@nestjs/common';
1
2
  import type { Controller, INestApplication } from '@nestjs/common/interfaces';
2
3
  import type { AbstractHttpAdapter } from '@nestjs/core';
3
4
  import type { MicroserviceOptions } from '@nestjs/microservices';
@@ -21,6 +22,18 @@ export interface ITransport {
21
22
  */
22
23
  getProtocol(): string;
23
24
 
25
+ /**
26
+ * Detect is it context of this transport
27
+ * !!! Do NOT check on getType() === 'rpc', this type corresponds to all microservices
28
+ *
29
+ * @param {ArgumentsHost} ctx
30
+ * @returns {boolean}
31
+ * @example
32
+ * return ctx.getType() === 'http'
33
+ * return ctx.getArgByIndex(1) instanceof MyTransportContext
34
+ */
35
+ matchByContext(ctx: ArgumentsHost): boolean;
36
+
24
37
  /**
25
38
  * @returns error formatting algorithm
26
39
  */
@@ -50,6 +63,7 @@ export interface ITransport {
50
63
  */
51
64
  export interface IPrimaryTransport extends ITransport {
52
65
  getHealthController(): Constructor<Controller>;
66
+
53
67
  getMetricsController(): Constructor<Controller>;
54
68
  }
55
69
 
@@ -105,6 +119,7 @@ export interface IHttpTransport extends IPrimaryTransport {
105
119
  app: INestApplication,
106
120
  configContext: ConfigContext,
107
121
  ): Promise<void> | void;
122
+
108
123
  createHttpOptions(configContext: ConfigContext): HttpOptions;
109
124
  }
110
125
 
package/tsconfig.json CHANGED
@@ -4,6 +4,16 @@
4
4
  "declaration": true,
5
5
  "outDir": "dist"
6
6
  },
7
- "include": ["src/**/*"],
8
- "exclude": ["node_modules", "dist", "test", "**/*.spec.ts", "**/*.test.ts", "**/*.spec.e2e.ts", "**/*.test.e2e.ts"]
7
+ "include": [
8
+ "src/**/*"
9
+ ],
10
+ "exclude": [
11
+ "node_modules",
12
+ "dist",
13
+ "test",
14
+ "**/*.spec.ts",
15
+ "**/*.test.ts",
16
+ "**/*.spec.e2e.ts",
17
+ "**/*.test.e2e.ts"
18
+ ]
9
19
  }