@rsdk/core 4.1.0 → 4.2.0-next.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 +12 -0
- package/dist/app-metadata/app-metadata.const.d.ts +1 -0
- package/dist/app-metadata/app-metadata.const.js +2 -1
- package/dist/app-metadata/app-metadata.const.js.map +1 -1
- package/dist/app-metadata/app-metadata.module.js +4 -0
- package/dist/app-metadata/app-metadata.module.js.map +1 -1
- package/dist/app-metadata/app-name.const.js +1 -1
- package/dist/app-metadata/app-name.const.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/platform.context.js +12 -4
- package/dist/platform.context.js.map +1 -1
- package/dist/platform.module.js +6 -1
- package/dist/platform.module.js.map +1 -1
- package/dist/tracing/auto-instrumentations.config.js +2 -3
- package/dist/tracing/auto-instrumentations.config.js.map +1 -1
- package/dist/tracing/grpc.headers.d.ts +11 -0
- package/dist/tracing/grpc.headers.js +24 -0
- package/dist/tracing/grpc.headers.js.map +1 -0
- package/dist/tracing/http.headers.d.ts +11 -0
- package/dist/tracing/http.headers.js +22 -0
- package/dist/tracing/http.headers.js.map +1 -0
- package/dist/tracing/index.d.ts +0 -1
- package/dist/tracing/index.js +0 -1
- package/dist/tracing/index.js.map +1 -1
- package/dist/tracing/open-telemetry.interceptor.d.ts +5 -0
- package/dist/tracing/open-telemetry.interceptor.js +83 -0
- package/dist/tracing/open-telemetry.interceptor.js.map +1 -0
- package/dist/tracing/request-metadata.module.js +1 -5
- package/dist/tracing/request-metadata.module.js.map +1 -1
- package/dist/tracing/services/instrumentation.service.d.ts +0 -1
- package/dist/tracing/services/instrumentation.service.js +0 -7
- package/dist/tracing/services/instrumentation.service.js.map +1 -1
- package/dist/tracing/services/request-id.provider.d.ts +11 -0
- package/dist/tracing/services/request-id.provider.js +13 -0
- package/dist/tracing/services/request-id.provider.js.map +1 -0
- package/dist/tracing/services/trace.injector.d.ts +4 -4
- package/dist/tracing/services/trace.injector.js +1 -132
- package/dist/tracing/services/trace.injector.js.map +1 -1
- package/dist/tracing/tracing.interceptor.d.ts +6 -5
- package/dist/tracing/tracing.interceptor.js +92 -7
- package/dist/tracing/tracing.interceptor.js.map +1 -1
- package/dist/tracing/tracing.module.d.ts +2 -2
- package/dist/tracing/tracing.module.js +8 -9
- package/dist/tracing/tracing.module.js.map +1 -1
- package/dist/types/options.d.ts +3 -1
- package/package.json +11 -10
- package/src/app-metadata/app-metadata.const.ts +1 -0
- package/src/app-metadata/app-metadata.module.ts +10 -1
- package/src/app-metadata/app-name.const.ts +1 -1
- package/src/index.ts +3 -3
- package/src/platform.context.ts +9 -1
- package/src/platform.module.ts +7 -1
- package/src/tracing/auto-instrumentations.config.ts +2 -3
- package/src/tracing/grpc.headers.ts +29 -0
- package/src/tracing/http.headers.ts +32 -0
- package/src/tracing/index.ts +0 -1
- package/src/tracing/open-telemetry.interceptor.ts +114 -0
- package/src/tracing/request-metadata.module.ts +1 -6
- package/src/tracing/services/instrumentation.service.ts +0 -11
- package/src/tracing/services/request-id.provider.ts +21 -0
- package/src/tracing/services/trace.injector.ts +4 -170
- package/src/tracing/tracing.interceptor.ts +131 -5
- package/src/tracing/tracing.module.ts +10 -12
- package/src/types/options.ts +2 -1
- package/dist/tracing/services/request-metadata.injector.d.ts +0 -6
- package/dist/tracing/services/request-metadata.injector.js +0 -123
- package/dist/tracing/services/request-metadata.injector.js.map +0 -1
- package/dist/tracing/services/request-metadata.storage.d.ts +0 -32
- package/dist/tracing/services/request-metadata.storage.js +0 -64
- package/dist/tracing/services/request-metadata.storage.js.map +0 -1
- package/src/tracing/services/request-metadata.injector.ts +0 -157
- package/src/tracing/services/request-metadata.storage.ts +0 -69
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { MaybeReadonlyArray } from '@rsdk/common.node';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @description хелпер для нормализованного извлечения заголовков из http запроса
|
|
5
|
+
*/
|
|
6
|
+
export class HttpHeaders {
|
|
7
|
+
constructor(private headers: Record<string, string>) {
|
|
8
|
+
this.normalizedHeaders = Object.fromEntries(
|
|
9
|
+
Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]),
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get<K extends string>(
|
|
14
|
+
key: MaybeReadonlyArray<K>,
|
|
15
|
+
): Record<K, string | undefined>;
|
|
16
|
+
|
|
17
|
+
get(key: string): string | undefined;
|
|
18
|
+
|
|
19
|
+
get(
|
|
20
|
+
key: string | MaybeReadonlyArray<string>,
|
|
21
|
+
): Record<string, string | undefined> | string | undefined {
|
|
22
|
+
if (Array.isArray(key)) {
|
|
23
|
+
return Object.fromEntries(key.map((k) => [k, this.get(k)])) as Record<
|
|
24
|
+
string,
|
|
25
|
+
any
|
|
26
|
+
>;
|
|
27
|
+
}
|
|
28
|
+
return this.normalizedHeaders[(key as string).toLowerCase()];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private normalizedHeaders: Record<string, string>;
|
|
32
|
+
}
|
package/src/tracing/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export * from './active-span.module';
|
|
2
2
|
export * from './request-metadata.module';
|
|
3
3
|
export * from './services/active-span.storage';
|
|
4
|
-
export * from './services/request-metadata.storage';
|
|
5
4
|
export * from './tracing.module';
|
|
6
5
|
export * from './utils/create-span';
|
|
7
6
|
export * from './utils/save-async-hooks-context';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CallHandler,
|
|
3
|
+
ExecutionContext,
|
|
4
|
+
NestInterceptor,
|
|
5
|
+
} from '@nestjs/common';
|
|
6
|
+
import { trace } from '@opentelemetry/api';
|
|
7
|
+
import {
|
|
8
|
+
X_B3_PARENT_SPAN_ID,
|
|
9
|
+
X_B3_SPAN_ID,
|
|
10
|
+
X_B3_TRACE_ID,
|
|
11
|
+
} from '@opentelemetry/propagator-b3';
|
|
12
|
+
import { api } from '@opentelemetry/sdk-node';
|
|
13
|
+
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
|
|
14
|
+
import { catchError, mergeMap, Observable, of, throwError } from 'rxjs';
|
|
15
|
+
|
|
16
|
+
import { OpenTelemetryProvider } from './services/request-id.provider';
|
|
17
|
+
import { TraceInjector } from './services';
|
|
18
|
+
|
|
19
|
+
export class OpenTelemetryInterceptor implements NestInterceptor {
|
|
20
|
+
intercept(
|
|
21
|
+
_context: ExecutionContext,
|
|
22
|
+
next: CallHandler<any>,
|
|
23
|
+
): Observable<any> | Promise<Observable<any>> {
|
|
24
|
+
const tracer = trace.getTracer('@rsdk/open-telemetry', '1.0.0');
|
|
25
|
+
|
|
26
|
+
const spanName = TraceInjector.createSpanName(
|
|
27
|
+
OpenTelemetryInterceptor.name,
|
|
28
|
+
'intercept',
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Создаем новый спан
|
|
33
|
+
*/
|
|
34
|
+
const span = tracer.startSpan(spanName, {});
|
|
35
|
+
|
|
36
|
+
const openTelemetryValues = OpenTelemetryProvider.getOrThrow();
|
|
37
|
+
|
|
38
|
+
const parentSpanId = openTelemetryValues[X_B3_PARENT_SPAN_ID];
|
|
39
|
+
if (parentSpanId) {
|
|
40
|
+
(span as any).parentSpanId = parentSpanId;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const spanId = openTelemetryValues[X_B3_SPAN_ID];
|
|
44
|
+
/**
|
|
45
|
+
* После создания контекста ранее при входе в приложение, у нас новый спан ид
|
|
46
|
+
* мы перетираем значением которе получили через заголовок
|
|
47
|
+
* в рамках приложения он корректный, но при переходе из одного приложения в другой - слетает
|
|
48
|
+
* и чтобы 100% все было норм, мы всегда патчим его
|
|
49
|
+
*/
|
|
50
|
+
if (spanId) {
|
|
51
|
+
span.spanContext().spanId = spanId;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const traceId = openTelemetryValues[X_B3_TRACE_ID];
|
|
55
|
+
/**
|
|
56
|
+
* Патчим трайс ид, причина выше
|
|
57
|
+
*/
|
|
58
|
+
if (traceId) {
|
|
59
|
+
span.spanContext().traceId = traceId;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Обычно самая первая точка входа это фронтовый запрос, который содержит некие заголовки хттп
|
|
63
|
+
* мы их кладем в рутовый спан
|
|
64
|
+
*/
|
|
65
|
+
if (openTelemetryValues.http) {
|
|
66
|
+
span.setAttribute(
|
|
67
|
+
SemanticAttributes.HTTP_METHOD,
|
|
68
|
+
openTelemetryValues.http.httpMethod,
|
|
69
|
+
);
|
|
70
|
+
span.setAttribute(
|
|
71
|
+
SemanticAttributes.HTTP_URL,
|
|
72
|
+
openTelemetryValues.http.httpUrl,
|
|
73
|
+
);
|
|
74
|
+
span.setAttribute(
|
|
75
|
+
SemanticAttributes.HTTP_ROUTE,
|
|
76
|
+
openTelemetryValues.http.httpRoute,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Чтобы пробросить пропатченный спан нужно запустить две строчки ниже:
|
|
81
|
+
* 1) создаем контекст в котором активный спан перебиваем новым
|
|
82
|
+
* 2) созданный контекст ставим основным и в нем запускаем под процесс
|
|
83
|
+
*/
|
|
84
|
+
const spanContext = api.trace.setSpan(api.context.active(), span);
|
|
85
|
+
|
|
86
|
+
return new Observable((observer) => {
|
|
87
|
+
api.context.with(spanContext, () => {
|
|
88
|
+
next
|
|
89
|
+
.handle()
|
|
90
|
+
.pipe(
|
|
91
|
+
mergeMap((result) => {
|
|
92
|
+
TraceInjector.enrich(span, result);
|
|
93
|
+
span.end();
|
|
94
|
+
return of(result);
|
|
95
|
+
}),
|
|
96
|
+
catchError((error) => {
|
|
97
|
+
TraceInjector.recordAndRethrow(error, span);
|
|
98
|
+
span.end();
|
|
99
|
+
return throwError(() => error);
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
.subscribe({
|
|
103
|
+
complete: () => {
|
|
104
|
+
observer.complete();
|
|
105
|
+
},
|
|
106
|
+
error: (error) => {
|
|
107
|
+
observer.error(error);
|
|
108
|
+
},
|
|
109
|
+
next: (res) => observer.next(res),
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { Global, Module } from '@nestjs/common';
|
|
2
2
|
|
|
3
|
-
import { RequestMetadataStorage } from './services/request-metadata.storage';
|
|
4
|
-
|
|
5
3
|
/**
|
|
6
4
|
* Модуль для хранения и получения данных из аскин локал стораджа
|
|
7
5
|
*/
|
|
8
6
|
@Global()
|
|
9
|
-
@Module({
|
|
10
|
-
providers: [RequestMetadataStorage],
|
|
11
|
-
exports: [RequestMetadataStorage],
|
|
12
|
-
})
|
|
7
|
+
@Module({})
|
|
13
8
|
export class RequestMetadataModule {}
|
|
@@ -2,21 +2,10 @@ import { Injectable } from '@nestjs/common';
|
|
|
2
2
|
|
|
3
3
|
import { TracingInterceptor } from '../tracing.interceptor';
|
|
4
4
|
|
|
5
|
-
import { RequestMetadataInjector } from './request-metadata.injector';
|
|
6
5
|
import { TraceInjector } from './trace.injector';
|
|
7
6
|
|
|
8
7
|
@Injectable()
|
|
9
8
|
export class InstrumentationService {
|
|
10
|
-
public injectWrapRequestMetadataInjector(): void {
|
|
11
|
-
/**
|
|
12
|
-
* Оборачиваем интерцептор в логику которая несет с собою асинк локал сторадж с requestId
|
|
13
|
-
*/
|
|
14
|
-
RequestMetadataInjector.wrap(
|
|
15
|
-
TracingInterceptor.prototype,
|
|
16
|
-
TracingInterceptor.prototype['intercept'],
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
9
|
public injectWrapTraceInjector(): void {
|
|
21
10
|
/**
|
|
22
11
|
* Оборачиваем интерцептор в логику которая несет с собою асинк локал сторадж с traceId, spanId
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
X_B3_PARENT_SPAN_ID,
|
|
3
|
+
X_B3_SPAN_ID,
|
|
4
|
+
X_B3_TRACE_ID,
|
|
5
|
+
} from '@opentelemetry/propagator-b3';
|
|
6
|
+
import { createAsyncContextProvider } from '@rsdk/actx';
|
|
7
|
+
|
|
8
|
+
export const RequestIdProvider = createAsyncContextProvider<string>({
|
|
9
|
+
name: 'RequestId',
|
|
10
|
+
rewritable: false,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const OpenTelemetryProvider = createAsyncContextProvider<{
|
|
14
|
+
[X_B3_SPAN_ID]?: string;
|
|
15
|
+
[X_B3_TRACE_ID]?: string;
|
|
16
|
+
[X_B3_PARENT_SPAN_ID]?: string;
|
|
17
|
+
http?: { httpMethod: string; httpUrl: string; httpRoute: string };
|
|
18
|
+
}>({
|
|
19
|
+
name: 'OpenTelemetryPropagation',
|
|
20
|
+
rewritable: false,
|
|
21
|
+
});
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import type { ExecutionContext } from '@nestjs/common';
|
|
2
1
|
import type { AttributeValue, Span } from '@opentelemetry/api';
|
|
3
2
|
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
3
|
import { api } from '@opentelemetry/sdk-node';
|
|
10
|
-
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
|
|
11
4
|
import type { ErrorLike } from '@rsdk/common';
|
|
12
5
|
import { isPrimitive, isRecord, normalizer } from '@rsdk/common';
|
|
13
6
|
import { redecorate } from '@rsdk/decorators';
|
|
@@ -82,165 +75,23 @@ export class TraceInjector {
|
|
|
82
75
|
private static createWrapper(
|
|
83
76
|
original: any,
|
|
84
77
|
spanName: string,
|
|
85
|
-
|
|
78
|
+
_isTracingInterceptor?: boolean,
|
|
86
79
|
): any {
|
|
87
80
|
return {
|
|
88
81
|
[original.name](...args: any[]): any {
|
|
89
82
|
const tracer = trace.getTracer('@rsdk/open-telemetry', '1.0.0');
|
|
90
|
-
const executionContext: ExecutionContext = args[0];
|
|
91
83
|
|
|
92
84
|
/**
|
|
93
85
|
* Переменный для хранения значений которые получаем для опен телеметрии
|
|
94
86
|
*/
|
|
95
87
|
let traceId: string | undefined;
|
|
96
88
|
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
|
-
<Record<string, unknown>>{},
|
|
173
|
-
),
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (req?.headers) {
|
|
180
|
-
/**
|
|
181
|
-
* Мы приводим все ключи заголовков к одному **lower_case** регистру, так как у разных транспортов разные регистры в именовании ключей для заголовков
|
|
182
|
-
*/
|
|
183
|
-
req.headers = {
|
|
184
|
-
...Object.entries(req.headers).reduce(
|
|
185
|
-
(acc, [key, value]) => {
|
|
186
|
-
acc[key.toLowerCase()] =
|
|
187
|
-
req.headers[key.toLowerCase()] || value;
|
|
188
|
-
return acc;
|
|
189
|
-
},
|
|
190
|
-
<Record<string, unknown>>{},
|
|
191
|
-
),
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Трейс ид - входящий рутовый идентификатор, он по всем цепочкам идет
|
|
196
|
-
*/
|
|
197
|
-
traceId = req.headers[X_B3_TRACE_ID];
|
|
198
|
-
/**
|
|
199
|
-
* Спан ид - идентификатор который нужен для построения цепочки вызовов
|
|
200
|
-
*/
|
|
201
|
-
spanId = req.headers[X_B3_SPAN_ID];
|
|
202
|
-
/**
|
|
203
|
-
* Флаг определяет находимся ли мы на рутовой позиции (самая первая точка входа)
|
|
204
|
-
* определяется просто - если с клиента не передали заголовок X_B3_PARENT_SPAN_ID то значит мы в точке входа,
|
|
205
|
-
* иначе мы являемся частью другой общей цепочки
|
|
206
|
-
*/
|
|
207
|
-
hasParentSpanId = !!Object.getOwnPropertyDescriptor(
|
|
208
|
-
req.headers,
|
|
209
|
-
X_B3_PARENT_SPAN_ID,
|
|
210
|
-
);
|
|
211
|
-
/**
|
|
212
|
-
* Идентификатор родительского спана - больше нужен для определения рутового состояния в различных кодах по опен телеметрии
|
|
213
|
-
*/
|
|
214
|
-
parentSpanId = req.headers[X_B3_PARENT_SPAN_ID];
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (isTracingInterceptor) {
|
|
218
|
-
/**
|
|
219
|
-
* Когда мы находимся в интерцепторе, то названием спана является название класса + метод интерцептора
|
|
220
|
-
* мы перебиваем на название класса + метод который трекаем
|
|
221
|
-
*/
|
|
222
|
-
spanName = TraceInjector.createSpanName(
|
|
223
|
-
executionContext.getClass().name,
|
|
224
|
-
executionContext.getHandler().name,
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
89
|
|
|
228
90
|
/**
|
|
229
91
|
* Создаем новый спан
|
|
230
92
|
*/
|
|
231
93
|
const span = tracer.startSpan(spanName, {});
|
|
232
94
|
|
|
233
|
-
// path parentSpanId for correct link to parent span
|
|
234
|
-
if (isTracingInterceptor && hasParentSpanId && parentSpanId) {
|
|
235
|
-
/**
|
|
236
|
-
* Так как опентелеметрия ставит свой некий парент спан ид, то наш слетает
|
|
237
|
-
* если мы работаем в рамках монолита то ничего не слетает
|
|
238
|
-
* как только мы перемещаемся между приложениями - он слетает
|
|
239
|
-
* чтобы иметь корректный парент спан ид - мы патчим его жестко (из коробки нельзя это сделать)
|
|
240
|
-
*/
|
|
241
|
-
(span as any).parentSpanId = parentSpanId;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
95
|
/**
|
|
245
96
|
* После создания контекста ранее при входе в приложение, у нас новый спан ид
|
|
246
97
|
* мы перетираем значением которе получили через заголовок
|
|
@@ -257,23 +108,6 @@ export class TraceInjector {
|
|
|
257
108
|
if (traceId) {
|
|
258
109
|
span.spanContext().traceId = traceId;
|
|
259
110
|
}
|
|
260
|
-
|
|
261
|
-
if (req?.headers) {
|
|
262
|
-
/**
|
|
263
|
-
* Обычно самая первая точка входа это фронтовый запрос, который содержит некие заголовки хттп
|
|
264
|
-
* мы их кладем в рутовый спан
|
|
265
|
-
*/
|
|
266
|
-
span.setAttribute(SemanticAttributes.HTTP_METHOD, req.method);
|
|
267
|
-
span.setAttribute(
|
|
268
|
-
SemanticAttributes.HTTP_URL,
|
|
269
|
-
req.originalUrl || req.url,
|
|
270
|
-
);
|
|
271
|
-
span.setAttribute(
|
|
272
|
-
SemanticAttributes.HTTP_ROUTE,
|
|
273
|
-
req.route?.path || req.routeOptions?.url || req.routerPath,
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
111
|
/**
|
|
278
112
|
* Чтобы пробросить пропатченный спан нужно запустить две строчки ниже:
|
|
279
113
|
* 1) создаем контекст в котором активный спан перебиваем новым
|
|
@@ -352,7 +186,7 @@ export class TraceInjector {
|
|
|
352
186
|
}[original.name];
|
|
353
187
|
}
|
|
354
188
|
|
|
355
|
-
|
|
189
|
+
static createSpanName(className: string, methodName: string): string {
|
|
356
190
|
/**
|
|
357
191
|
* Всего лишь правила формирования имени спана (не более того), для новой логики нужен был именно такой код
|
|
358
192
|
*/
|
|
@@ -380,7 +214,7 @@ export class TraceInjector {
|
|
|
380
214
|
Reflect.defineMetadata(Constants.TRACE_METADATA_ACTIVE, NOOP, prototype);
|
|
381
215
|
}
|
|
382
216
|
|
|
383
|
-
|
|
217
|
+
static recordAndRethrow(error: unknown, span: Span): void {
|
|
384
218
|
span.recordException(error as ErrorLike);
|
|
385
219
|
span.setStatus({
|
|
386
220
|
code: SpanStatusCode.ERROR,
|
|
@@ -390,7 +224,7 @@ export class TraceInjector {
|
|
|
390
224
|
throw error;
|
|
391
225
|
}
|
|
392
226
|
|
|
393
|
-
|
|
227
|
+
static enrich(span: Span, data: unknown): unknown {
|
|
394
228
|
span.setAttribute('response', TraceInjector.toAttribute(data));
|
|
395
229
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
396
230
|
|
|
@@ -4,15 +4,141 @@ import type {
|
|
|
4
4
|
NestInterceptor,
|
|
5
5
|
} from '@nestjs/common';
|
|
6
6
|
import { Injectable } from '@nestjs/common';
|
|
7
|
+
import {
|
|
8
|
+
X_B3_PARENT_SPAN_ID,
|
|
9
|
+
X_B3_SPAN_ID,
|
|
10
|
+
X_B3_TRACE_ID,
|
|
11
|
+
} from '@opentelemetry/propagator-b3';
|
|
12
|
+
import { omitUndefined } from '@rsdk/common.node';
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
7
14
|
import type { Observable } from 'rxjs';
|
|
8
15
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
import {
|
|
17
|
+
OpenTelemetryProvider,
|
|
18
|
+
RequestIdProvider,
|
|
19
|
+
} from './services/request-id.provider';
|
|
20
|
+
import { X_REQUEST_ID } from './constants';
|
|
21
|
+
import { GrpcHeaders } from './grpc.headers';
|
|
22
|
+
import { HttpHeaders } from './http.headers';
|
|
23
|
+
|
|
24
|
+
type TracingHeaders = {
|
|
25
|
+
[X_REQUEST_ID]: string;
|
|
26
|
+
[X_B3_SPAN_ID]?: string;
|
|
27
|
+
[X_B3_TRACE_ID]?: string;
|
|
28
|
+
[X_B3_PARENT_SPAN_ID]?: string;
|
|
29
|
+
http?: { httpMethod: string; httpUrl: string; httpRoute: string };
|
|
30
|
+
};
|
|
31
|
+
const tracingHeaders = [
|
|
32
|
+
X_REQUEST_ID,
|
|
33
|
+
X_B3_TRACE_ID,
|
|
34
|
+
X_B3_SPAN_ID,
|
|
35
|
+
X_B3_PARENT_SPAN_ID,
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
13
38
|
@Injectable()
|
|
14
39
|
export class TracingInterceptor implements NestInterceptor {
|
|
15
|
-
intercept(
|
|
40
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
|
41
|
+
const requestId = TracingInterceptor.extractTracingHeaders(context);
|
|
42
|
+
|
|
43
|
+
RequestIdProvider.set(requestId[X_REQUEST_ID]);
|
|
44
|
+
OpenTelemetryProvider.set(omitUndefined(requestId));
|
|
16
45
|
return next.handle();
|
|
17
46
|
}
|
|
47
|
+
|
|
48
|
+
private static extractTracingHeaders(
|
|
49
|
+
executionContext: ExecutionContext,
|
|
50
|
+
): TracingHeaders {
|
|
51
|
+
/**
|
|
52
|
+
* В зависимости от типа контекста запускаем парсинг переданной Metadata
|
|
53
|
+
*/
|
|
54
|
+
switch (executionContext.getType<string>()) {
|
|
55
|
+
case 'rpc':
|
|
56
|
+
return this.extractFromRpc(executionContext);
|
|
57
|
+
case 'http':
|
|
58
|
+
return this.extractFromHttp(executionContext);
|
|
59
|
+
case 'graphql':
|
|
60
|
+
return this.extractFromGraphql(executionContext);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
[X_REQUEST_ID]: this.createRequestId(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private static extractFromGraphql(
|
|
68
|
+
executionContext: ExecutionContext,
|
|
69
|
+
): TracingHeaders {
|
|
70
|
+
const req = executionContext.getArgs()[2]?.req; // аналог GqlExecutionContext.create(executionContext).getContext().req
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* При работе с сабскрипшен через веб сокет, заголовки передаются в опции подключения к сабскрипшен в переменную connectionParams
|
|
74
|
+
* и мы перегоняем эти данные в request заголовки
|
|
75
|
+
*/
|
|
76
|
+
const headers = {
|
|
77
|
+
...req?.headers,
|
|
78
|
+
...req?.connectionParams?.headers,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const extractedHeaders = new HttpHeaders(headers).get([
|
|
82
|
+
X_REQUEST_ID,
|
|
83
|
+
X_B3_TRACE_ID,
|
|
84
|
+
X_B3_SPAN_ID,
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
const h = extractedHeaders as TracingHeaders;
|
|
88
|
+
|
|
89
|
+
h.http = {
|
|
90
|
+
httpMethod: req.method!,
|
|
91
|
+
httpRoute: req.route?.path || req.routeOptions?.url || req.routerPath,
|
|
92
|
+
httpUrl: req.originalUrl || req.url,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
h[X_REQUEST_ID] ??= randomUUID();
|
|
96
|
+
return h as TracingHeaders;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private static extractFromHttp(
|
|
100
|
+
executionContext: ExecutionContext,
|
|
101
|
+
): TracingHeaders {
|
|
102
|
+
// TODO:
|
|
103
|
+
const req = executionContext.switchToHttp().getRequest<any>();
|
|
104
|
+
|
|
105
|
+
const headers = new HttpHeaders(req.headers as Record<string, string>).get(
|
|
106
|
+
tracingHeaders,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const h: TracingHeaders = headers as TracingHeaders;
|
|
110
|
+
|
|
111
|
+
h[X_REQUEST_ID] ??= randomUUID();
|
|
112
|
+
h.http = {
|
|
113
|
+
httpMethod: req.method!,
|
|
114
|
+
httpRoute: req.route?.path || req.routeOptions?.url || req.routerPath,
|
|
115
|
+
httpUrl: req.originalUrl || req.url,
|
|
116
|
+
};
|
|
117
|
+
return headers as TracingHeaders;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private static extractFromRpc(
|
|
121
|
+
executionContext: ExecutionContext,
|
|
122
|
+
): TracingHeaders {
|
|
123
|
+
const metadata = executionContext.switchToRpc().getContext();
|
|
124
|
+
/**
|
|
125
|
+
* Если мы пришли в GRPC и у нас есть методы для работы с Metadata
|
|
126
|
+
* то мы конвертируем Metadata в некий виртуальный request с заголовками (заголовки выбраны как общий стандарт проброса мета информации)
|
|
127
|
+
*/
|
|
128
|
+
if (metadata?.get) {
|
|
129
|
+
const grpcHeaders = new GrpcHeaders(metadata);
|
|
130
|
+
|
|
131
|
+
const h = grpcHeaders.get(tracingHeaders);
|
|
132
|
+
|
|
133
|
+
h[X_REQUEST_ID] ??= this.createRequestId();
|
|
134
|
+
return h as TracingHeaders;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
[X_REQUEST_ID]: randomUUID(),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private static createRequestId(): string {
|
|
142
|
+
return randomUUID();
|
|
143
|
+
}
|
|
18
144
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DynamicModule, Provider } from '@nestjs/common';
|
|
1
|
+
import type { DynamicModule, OnModuleInit, Provider } from '@nestjs/common';
|
|
2
2
|
import { Module } from '@nestjs/common';
|
|
3
3
|
import { APP_INTERCEPTOR } from '@nestjs/core';
|
|
4
4
|
import { context, trace } from '@opentelemetry/api';
|
|
@@ -18,9 +18,10 @@ import { ILogger, LoggerFactory } from '@rsdk/logging';
|
|
|
18
18
|
|
|
19
19
|
import { InjectLogger } from '../logging';
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import { RequestIdProvider } from './services/request-id.provider';
|
|
22
22
|
import { ActiveSpanModule } from './active-span.module';
|
|
23
23
|
import { autoInstumentationOptions } from './auto-instrumentations.config';
|
|
24
|
+
import { OpenTelemetryInterceptor } from './open-telemetry.interceptor';
|
|
24
25
|
import { RequestMetadataModule } from './request-metadata.module';
|
|
25
26
|
import { InstrumentationService } from './services';
|
|
26
27
|
import { TracingModuleConfig } from './tracing.config';
|
|
@@ -33,7 +34,7 @@ export interface TracingModuleOptions {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
@Module({})
|
|
36
|
-
export class TracingModule {
|
|
37
|
+
export class TracingModule implements OnModuleInit {
|
|
37
38
|
constructor(
|
|
38
39
|
@InjectLogger(TracingModule) private readonly logger: ILogger,
|
|
39
40
|
private readonly instrumentations: InstrumentationService,
|
|
@@ -46,27 +47,24 @@ export class TracingModule {
|
|
|
46
47
|
module: TracingModule,
|
|
47
48
|
imports: [RequestMetadataModule, ActiveSpanModule],
|
|
48
49
|
providers: [
|
|
50
|
+
{ provide: APP_INTERCEPTOR, useValue: new TracingInterceptor() },
|
|
51
|
+
{
|
|
52
|
+
provide: APP_INTERCEPTOR,
|
|
53
|
+
useValue: new OpenTelemetryInterceptor(),
|
|
54
|
+
},
|
|
49
55
|
InstrumentationService,
|
|
50
56
|
this.createSDKProvider(options),
|
|
51
|
-
/**
|
|
52
|
-
* Глобальный интерцептор для проброса входящих traceId, spanId и requestId из заголовков и метадаты в активный AsyncLocalStorage запроса
|
|
53
|
-
*/
|
|
54
|
-
{ provide: APP_INTERCEPTOR, useClass: TracingInterceptor },
|
|
55
57
|
],
|
|
56
58
|
};
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
// Можно добавить включение и выключение при изменении конфига.
|
|
60
62
|
async onModuleInit(): Promise<void> {
|
|
61
|
-
this.logger.debug('Injecting request-metadata storage...');
|
|
62
|
-
this.instrumentations.injectWrapRequestMetadataInjector();
|
|
63
|
-
|
|
64
63
|
if (!this.config.enabled) {
|
|
65
64
|
this.logger.info('Tracing is disabled');
|
|
66
65
|
|
|
67
66
|
LoggerFactory.applyInstrumentations((record) => {
|
|
68
|
-
record['request_id'] =
|
|
69
|
-
RequestMetadataStorage.getInstance()?.getRequestMetadata().requestId;
|
|
67
|
+
record['request_id'] = RequestIdProvider.get();
|
|
70
68
|
});
|
|
71
69
|
|
|
72
70
|
return;
|
package/src/types/options.ts
CHANGED