@blueking/open-telemetry 0.0.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/README.md +0 -0
- package/dist/bk-rum.global.js +11 -0
- package/dist/bk-rum.global.js.map +1 -0
- package/dist/index.js +1186 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/playground/App.vue +148 -0
- package/playground/bk-ot.ts +110 -0
- package/playground/components/DemoLogPanel.vue +89 -0
- package/playground/composables/use-demo-log.ts +39 -0
- package/playground/index.html +13 -0
- package/playground/main.ts +36 -0
- package/playground/mock/index.ts +43 -0
- package/playground/pages/BlankScreenDemo.vue +97 -0
- package/playground/pages/BusinessDemo.vue +74 -0
- package/playground/pages/CustomPluginDemo.vue +104 -0
- package/playground/pages/ErrorDemo.vue +75 -0
- package/playground/pages/Home.vue +133 -0
- package/playground/pages/HttpDemo.vue +120 -0
- package/playground/pages/LongTaskDemo.vue +66 -0
- package/playground/pages/WebSocketDemo.vue +100 -0
- package/playground/router/index.ts +92 -0
- package/playground/style.css +180 -0
- package/src/browser.ts +44 -0
- package/src/core/config.ts +327 -0
- package/src/core/plugin.ts +97 -0
- package/src/core/processor.ts +74 -0
- package/src/core/route-observer.ts +128 -0
- package/src/core/sampling.ts +64 -0
- package/src/core/sdk.ts +327 -0
- package/src/core/url.ts +66 -0
- package/src/index.ts +55 -0
- package/src/plugins/blank-screen.ts +170 -0
- package/src/plugins/common.ts +131 -0
- package/src/plugins/csp-violation.ts +74 -0
- package/src/plugins/device.ts +91 -0
- package/src/plugins/error.ts +211 -0
- package/src/plugins/http-body.ts +437 -0
- package/src/plugins/long-task.ts +99 -0
- package/src/plugins/page-view.ts +83 -0
- package/src/plugins/route-timing.ts +89 -0
- package/src/plugins/session.ts +127 -0
- package/src/plugins/web-vitals.ts +159 -0
- package/src/plugins/websocket.ts +179 -0
- package/tsconfig.app.json +27 -0
- package/tsconfig.dts.json +13 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +70 -0
package/src/browser.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tencent is pleased to support the open source community by making
|
|
3
|
+
* 蓝鲸智云PaaS平台 (BlueKing PaaS) available.
|
|
4
|
+
*
|
|
5
|
+
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
|
6
|
+
*
|
|
7
|
+
* 蓝鲸智云PaaS平台 (BlueKing PaaS) is licensed under the MIT License.
|
|
8
|
+
*
|
|
9
|
+
* License for 蓝鲸智云PaaS平台 (BlueKing PaaS):
|
|
10
|
+
*
|
|
11
|
+
* ---------------------------------------------------
|
|
12
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
13
|
+
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
|
14
|
+
* the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
|
|
15
|
+
* to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
16
|
+
*
|
|
17
|
+
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of
|
|
18
|
+
* the Software.
|
|
19
|
+
*
|
|
20
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
|
|
21
|
+
* THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
23
|
+
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
24
|
+
* IN THE SOFTWARE.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { createPlugin } from './core/plugin';
|
|
28
|
+
import { createBkOT, initBkOT } from './core/sdk';
|
|
29
|
+
|
|
30
|
+
export * from './index';
|
|
31
|
+
|
|
32
|
+
const BkOpenTelemetry = {
|
|
33
|
+
createBkOT,
|
|
34
|
+
createPlugin,
|
|
35
|
+
initBkOT,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (typeof window !== 'undefined') {
|
|
39
|
+
Object.assign(window, {
|
|
40
|
+
BkOpenTelemetry,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { BkOpenTelemetry };
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tencent is pleased to support the open source community by making
|
|
3
|
+
* 蓝鲸智云PaaS平台 (BlueKing PaaS) available.
|
|
4
|
+
*
|
|
5
|
+
* Copyright (C) 2017-2025 Tencent. All rights reserved.
|
|
6
|
+
*
|
|
7
|
+
* 蓝鲸智云PaaS平台 (BlueKing PaaS) is licensed under the MIT License.
|
|
8
|
+
*
|
|
9
|
+
* License for 蓝鲸智云PaaS平台 (BlueKing PaaS):
|
|
10
|
+
*
|
|
11
|
+
* ---------------------------------------------------
|
|
12
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
13
|
+
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
|
14
|
+
* the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
|
|
15
|
+
* to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
16
|
+
*
|
|
17
|
+
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of
|
|
18
|
+
* the Software.
|
|
19
|
+
*
|
|
20
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
|
|
21
|
+
* THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
23
|
+
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
24
|
+
* IN THE SOFTWARE.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { BkOTPlugin } from './plugin';
|
|
28
|
+
import type { Attributes } from '@opentelemetry/api';
|
|
29
|
+
|
|
30
|
+
export interface BkOTAttributesConfig {
|
|
31
|
+
custom?: () => Attributes;
|
|
32
|
+
error?: () => Attributes;
|
|
33
|
+
metric?: () => Attributes;
|
|
34
|
+
page?: () => Attributes;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type BkOTAttributeValue = boolean | number | string;
|
|
38
|
+
|
|
39
|
+
export interface BkOTBatchConfig {
|
|
40
|
+
exportTimeoutMillis?: number;
|
|
41
|
+
maxExportBatchSize?: number;
|
|
42
|
+
maxQueueSize?: number;
|
|
43
|
+
scheduledDelayMillis?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BkOTConfig {
|
|
47
|
+
attributes?: BkOTAttributesConfig;
|
|
48
|
+
autoStart?: boolean;
|
|
49
|
+
debug?: boolean;
|
|
50
|
+
enabled?: boolean;
|
|
51
|
+
endpoint?: string;
|
|
52
|
+
environment?: string;
|
|
53
|
+
headers?: Record<string, string>;
|
|
54
|
+
plugins?: BkOTPlugin[];
|
|
55
|
+
redact?: BkOTRedactConfig;
|
|
56
|
+
rum?: BkOTRumConfig;
|
|
57
|
+
sampleRate?: number;
|
|
58
|
+
spanBatch?: BkOTBatchConfig;
|
|
59
|
+
token?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface BkOTCustomEventPayload {
|
|
63
|
+
attributes?: Attributes;
|
|
64
|
+
error?: Error;
|
|
65
|
+
name: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface BkOTHttpBodyConfig {
|
|
69
|
+
maxBodySize?: number;
|
|
70
|
+
redact?: (payload: BkOTHttpBodyRedactPayload) => string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface BkOTHttpBodyRedactPayload {
|
|
74
|
+
body: string;
|
|
75
|
+
contentType?: string;
|
|
76
|
+
method: string;
|
|
77
|
+
status?: number;
|
|
78
|
+
truncated: boolean;
|
|
79
|
+
type: 'request' | 'response';
|
|
80
|
+
url: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface BkOTInstrumentationsConfig {
|
|
84
|
+
documentLoad?: boolean;
|
|
85
|
+
fetch?: boolean;
|
|
86
|
+
userInteraction?: boolean | { eventNames?: string[] };
|
|
87
|
+
xhr?: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface BkOTRedactConfig {
|
|
91
|
+
// 用于隐私脱敏:在所有 RUM 自定义插件 emit 之前过滤一次属性
|
|
92
|
+
attributes?: (attributes: Attributes) => Attributes;
|
|
93
|
+
// 用于隐私脱敏:所有上报到 attribute 的 URL 在写入前会经过此函数
|
|
94
|
+
url?: (url: string) => string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface BkOTRumConfig extends BkOTInstrumentationsConfig {
|
|
98
|
+
cspViolation?: boolean;
|
|
99
|
+
device?: boolean | { storageKey?: string };
|
|
100
|
+
httpBody?: BkOTHttpBodyConfig | boolean;
|
|
101
|
+
pageView?: boolean;
|
|
102
|
+
routeTiming?: boolean;
|
|
103
|
+
websocket?: boolean;
|
|
104
|
+
webVitals?: boolean;
|
|
105
|
+
blankScreen?:
|
|
106
|
+
| {
|
|
107
|
+
checkDelay?: number;
|
|
108
|
+
// 自定义"页面已渲染"忽略选择器,命中即认为是 loading 占位、非空白
|
|
109
|
+
ignoreSelectors?: string[];
|
|
110
|
+
rootSelector?: string;
|
|
111
|
+
threshold?: number;
|
|
112
|
+
}
|
|
113
|
+
| boolean;
|
|
114
|
+
error?:
|
|
115
|
+
| {
|
|
116
|
+
// 同 hash 错误窗口内最多上报多少条,默认 5
|
|
117
|
+
maxPerWindow?: number;
|
|
118
|
+
// 节流窗口长度(ms),默认 60_000
|
|
119
|
+
windowMs?: number;
|
|
120
|
+
}
|
|
121
|
+
| boolean;
|
|
122
|
+
longTask?:
|
|
123
|
+
| {
|
|
124
|
+
// 任务时长阈值(ms),低于该值的 longtask 不上报,默认 50
|
|
125
|
+
threshold?: number;
|
|
126
|
+
}
|
|
127
|
+
| boolean;
|
|
128
|
+
session?:
|
|
129
|
+
| {
|
|
130
|
+
// 不活跃多久后视为新会话,默认 30 分钟
|
|
131
|
+
inactivityMs?: number;
|
|
132
|
+
storageKey?: string;
|
|
133
|
+
}
|
|
134
|
+
| boolean;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface NormalizedBkOTConfig extends Omit<
|
|
138
|
+
BkOTConfig,
|
|
139
|
+
'attributes' | 'autoStart' | 'debug' | 'enabled' | 'endpoint' | 'environment' | 'plugins' | 'redact' | 'token'
|
|
140
|
+
> {
|
|
141
|
+
autoStart: boolean;
|
|
142
|
+
debug: boolean;
|
|
143
|
+
enabled: boolean;
|
|
144
|
+
endpoint: string;
|
|
145
|
+
environment: string;
|
|
146
|
+
logs: SignalExporterConfig;
|
|
147
|
+
metricIntervalMillis: number;
|
|
148
|
+
metrics: SignalExporterConfig;
|
|
149
|
+
plugins: BkOTPlugin[];
|
|
150
|
+
resourceAttributes: Attributes;
|
|
151
|
+
sampleRate: number;
|
|
152
|
+
traces: SignalExporterConfig;
|
|
153
|
+
getCustomAttributes: () => Attributes;
|
|
154
|
+
getErrorAttributes: () => Attributes;
|
|
155
|
+
getMetricAttributes: () => Attributes;
|
|
156
|
+
getPageAttributes: () => Attributes;
|
|
157
|
+
redactAttributes: (attributes: Attributes) => Attributes;
|
|
158
|
+
redactUrl: (url: string) => string;
|
|
159
|
+
instrumentations: Required<Pick<BkOTInstrumentationsConfig, 'documentLoad' | 'fetch' | 'xhr'>> & {
|
|
160
|
+
userInteraction: BkOTInstrumentationsConfig['userInteraction'];
|
|
161
|
+
};
|
|
162
|
+
rum: Omit<Required<BkOTRumConfig>, 'documentLoad' | 'fetch' | 'httpBody' | 'userInteraction' | 'xhr'> & {
|
|
163
|
+
httpBody: false | Required<BkOTHttpBodyConfig>;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface SignalExporterConfig {
|
|
168
|
+
concurrencyLimit?: number;
|
|
169
|
+
endpoint: string;
|
|
170
|
+
headers?: Record<string, string>;
|
|
171
|
+
timeoutMillis?: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const DEFAULT_OTLP_ENDPOINT = 'http://localhost:4318'; // 默认上报到本机 OTLP collector
|
|
175
|
+
const DEFAULT_METRIC_INTERVAL_MILLIS = 10 * 1000; // 10s 上报一次
|
|
176
|
+
|
|
177
|
+
const trimTrailingSlash = (value: string) => value.replace(/\/+$/, '');
|
|
178
|
+
|
|
179
|
+
const resolveSignalEndpoint = (endpoint: string | undefined, signalPath: 'logs' | 'metrics' | 'traces') => {
|
|
180
|
+
const normalized = trimTrailingSlash(endpoint || DEFAULT_OTLP_ENDPOINT);
|
|
181
|
+
if (normalized.endsWith(`/v1/${signalPath}`)) {
|
|
182
|
+
return normalized;
|
|
183
|
+
}
|
|
184
|
+
if (normalized.endsWith('/v1')) {
|
|
185
|
+
return `${normalized}/${signalPath}`;
|
|
186
|
+
}
|
|
187
|
+
return `${normalized}/v1/${signalPath}`;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const resolveSignalConfig = (base: BkOTConfig, signalPath: 'logs' | 'metrics' | 'traces'): SignalExporterConfig => {
|
|
191
|
+
// token 只负责提供默认鉴权头;headers 保持显式配置优先,便于特殊场景覆盖。
|
|
192
|
+
const mergedHeaders =
|
|
193
|
+
base.token || base.headers
|
|
194
|
+
? {
|
|
195
|
+
...(base.token ? { Authorization: `Bearer ${base.token}` } : {}),
|
|
196
|
+
...(base.headers ?? {}),
|
|
197
|
+
}
|
|
198
|
+
: undefined;
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
endpoint: resolveSignalEndpoint(base.endpoint, signalPath),
|
|
202
|
+
headers: mergedHeaders,
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const clampSampleRate = (sampleRate?: number) => {
|
|
207
|
+
if (typeof sampleRate !== 'number' || !Number.isFinite(sampleRate)) {
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
return Math.max(0, Math.min(1, sampleRate));
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const positiveNumber = (value: number | undefined, fallback: number) =>
|
|
214
|
+
typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
215
|
+
|
|
216
|
+
const nonNegativeNumber = (value: number | undefined, fallback: number) =>
|
|
217
|
+
typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
218
|
+
|
|
219
|
+
const boundedRatio = (value: number | undefined, fallback: number) => {
|
|
220
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
221
|
+
return fallback;
|
|
222
|
+
}
|
|
223
|
+
return Math.max(0, Math.min(1, value));
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const identityRedactHttpBody = (payload: BkOTHttpBodyRedactPayload) => payload.body;
|
|
227
|
+
|
|
228
|
+
const normalizeHttpBodyConfig = (httpBody: BkOTRumConfig['httpBody']): false | Required<BkOTHttpBodyConfig> => {
|
|
229
|
+
if (!httpBody) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
if (typeof httpBody === 'boolean') {
|
|
233
|
+
return {
|
|
234
|
+
maxBodySize: 10 * 1024,
|
|
235
|
+
redact: identityRedactHttpBody,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
maxBodySize: positiveNumber(httpBody.maxBodySize, 10 * 1024),
|
|
240
|
+
redact: httpBody.redact ?? identityRedactHttpBody,
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const normalizeRumConfig = (rum: BkOTConfig['rum']): NormalizedBkOTConfig['rum'] => ({
|
|
245
|
+
device: rum?.device ?? true,
|
|
246
|
+
httpBody: normalizeHttpBodyConfig(rum?.httpBody),
|
|
247
|
+
session:
|
|
248
|
+
typeof rum?.session === 'object'
|
|
249
|
+
? {
|
|
250
|
+
...rum.session,
|
|
251
|
+
inactivityMs: positiveNumber(rum.session.inactivityMs, 30 * 60 * 1000),
|
|
252
|
+
}
|
|
253
|
+
: (rum?.session ?? true),
|
|
254
|
+
pageView: rum?.pageView ?? true,
|
|
255
|
+
error:
|
|
256
|
+
typeof rum?.error === 'object'
|
|
257
|
+
? {
|
|
258
|
+
...rum.error,
|
|
259
|
+
maxPerWindow: positiveNumber(rum.error.maxPerWindow, 5),
|
|
260
|
+
windowMs: positiveNumber(rum.error.windowMs, 60_000),
|
|
261
|
+
}
|
|
262
|
+
: (rum?.error ?? true),
|
|
263
|
+
webVitals: rum?.webVitals ?? true,
|
|
264
|
+
blankScreen:
|
|
265
|
+
typeof rum?.blankScreen === 'object'
|
|
266
|
+
? {
|
|
267
|
+
...rum.blankScreen,
|
|
268
|
+
checkDelay: nonNegativeNumber(rum.blankScreen.checkDelay, 3000),
|
|
269
|
+
threshold: boundedRatio(rum.blankScreen.threshold, 0.8),
|
|
270
|
+
}
|
|
271
|
+
: (rum?.blankScreen ?? true),
|
|
272
|
+
websocket: rum?.websocket ?? true,
|
|
273
|
+
longTask:
|
|
274
|
+
typeof rum?.longTask === 'object'
|
|
275
|
+
? {
|
|
276
|
+
...rum.longTask,
|
|
277
|
+
threshold: nonNegativeNumber(rum.longTask.threshold, 50),
|
|
278
|
+
}
|
|
279
|
+
: (rum?.longTask ?? false),
|
|
280
|
+
cspViolation: rum?.cspViolation ?? false,
|
|
281
|
+
routeTiming: rum?.routeTiming ?? false,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const identityRedactAttributes = (attributes: Attributes) => attributes;
|
|
285
|
+
const identityRedactUrl = (url: string) => url;
|
|
286
|
+
|
|
287
|
+
export const normalizeConfig = (config: BkOTConfig): NormalizedBkOTConfig => {
|
|
288
|
+
const resourceAttributes: Attributes = {
|
|
289
|
+
'deployment.environment.name': config.environment ?? 'production',
|
|
290
|
+
'rum.provider': 'blueking',
|
|
291
|
+
'telemetry.sdk.language': 'webjs',
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
...config,
|
|
296
|
+
debug: config.debug ?? false,
|
|
297
|
+
enabled: config.enabled ?? true,
|
|
298
|
+
environment: config.environment ?? 'production',
|
|
299
|
+
endpoint: trimTrailingSlash(config.endpoint || DEFAULT_OTLP_ENDPOINT),
|
|
300
|
+
traces: resolveSignalConfig(config, 'traces'),
|
|
301
|
+
metrics: resolveSignalConfig(config, 'metrics'),
|
|
302
|
+
logs: resolveSignalConfig(config, 'logs'),
|
|
303
|
+
sampleRate: clampSampleRate(config.sampleRate),
|
|
304
|
+
resourceAttributes,
|
|
305
|
+
metricIntervalMillis: DEFAULT_METRIC_INTERVAL_MILLIS,
|
|
306
|
+
instrumentations: {
|
|
307
|
+
documentLoad: config.rum?.documentLoad ?? true,
|
|
308
|
+
fetch: config.rum?.fetch ?? true,
|
|
309
|
+
xhr: config.rum?.xhr ?? true,
|
|
310
|
+
userInteraction: config.rum?.userInteraction ?? true,
|
|
311
|
+
},
|
|
312
|
+
rum: normalizeRumConfig(config.rum),
|
|
313
|
+
plugins: config.plugins ?? [],
|
|
314
|
+
autoStart: config.autoStart ?? true,
|
|
315
|
+
getPageAttributes:
|
|
316
|
+
config.attributes?.page ??
|
|
317
|
+
(() => ({
|
|
318
|
+
'rum.page.host': typeof window === 'undefined' ? '' : window.location.host,
|
|
319
|
+
'rum.page.path': typeof window === 'undefined' ? '' : window.location.pathname,
|
|
320
|
+
})),
|
|
321
|
+
getMetricAttributes: config.attributes?.metric ?? (() => ({})),
|
|
322
|
+
getErrorAttributes: config.attributes?.error ?? (() => ({})),
|
|
323
|
+
getCustomAttributes: config.attributes?.custom ?? (() => ({})),
|
|
324
|
+
redactAttributes: config.redact?.attributes ?? identityRedactAttributes,
|
|
325
|
+
redactUrl: config.redact?.url ?? identityRedactUrl,
|
|
326
|
+
};
|
|
327
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tencent is pleased to support the open source community by making
|
|
3
|
+
* 蓝鲸智云PaaS平台 (BlueKing PaaS) available.
|
|
4
|
+
*
|
|
5
|
+
* Copyright (C) 2017-2025 Tencent. All rights reserved.
|
|
6
|
+
*
|
|
7
|
+
* 蓝鲸智云PaaS平台 (BlueKing PaaS) is licensed under the MIT License.
|
|
8
|
+
*
|
|
9
|
+
* License for 蓝鲸智云PaaS平台 (BlueKing PaaS):
|
|
10
|
+
*
|
|
11
|
+
* ---------------------------------------------------
|
|
12
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
13
|
+
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
|
14
|
+
* the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
|
|
15
|
+
* to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
16
|
+
*
|
|
17
|
+
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of
|
|
18
|
+
* the Software.
|
|
19
|
+
*
|
|
20
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
|
|
21
|
+
* THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
23
|
+
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
24
|
+
* IN THE SOFTWARE.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { NormalizedBkOTConfig } from './config';
|
|
28
|
+
import type { Attributes, Meter, Span, Tracer } from '@opentelemetry/api';
|
|
29
|
+
import type { Logger, LogRecord } from '@opentelemetry/api-logs';
|
|
30
|
+
|
|
31
|
+
export interface BkOTPlugin {
|
|
32
|
+
enabled?: ((config: NormalizedBkOTConfig) => boolean) | boolean;
|
|
33
|
+
name: string;
|
|
34
|
+
flush?: () => Promise<void> | void;
|
|
35
|
+
init: (context: BkOTRuntimeContext) => Promise<void> | void;
|
|
36
|
+
shutdown?: () => Promise<void> | void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BkOTRuntimeContext {
|
|
40
|
+
config: NormalizedBkOTConfig;
|
|
41
|
+
logger: Logger;
|
|
42
|
+
meter: Meter;
|
|
43
|
+
tracer: Tracer;
|
|
44
|
+
/** 对外提供的属性脱敏入口,所有插件在 emit 前都应过一次 */
|
|
45
|
+
applyRedact: (attributes: Attributes) => Attributes;
|
|
46
|
+
/** 包装 logger.emit,自动应用 redact 与 runtime attributes */
|
|
47
|
+
emitLog: (record: LogRecord) => void;
|
|
48
|
+
getRuntimeAttributes: () => Attributes;
|
|
49
|
+
setRuntimeAttributes: (attributes: Attributes) => void;
|
|
50
|
+
startSpan: (name: string, attributes?: Attributes) => Span;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const isPluginEnabled = (plugin: BkOTPlugin, config: NormalizedBkOTConfig) => {
|
|
54
|
+
if (typeof plugin.enabled === 'function') {
|
|
55
|
+
return plugin.enabled(config);
|
|
56
|
+
}
|
|
57
|
+
return plugin.enabled !== false;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const createPlugin = (plugin: BkOTPlugin) => plugin;
|
|
61
|
+
|
|
62
|
+
export class PluginManager {
|
|
63
|
+
private readonly plugins: BkOTPlugin[];
|
|
64
|
+
private startedPlugins: BkOTPlugin[] = [];
|
|
65
|
+
|
|
66
|
+
public constructor(plugins: BkOTPlugin[]) {
|
|
67
|
+
this.plugins = plugins;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public async flush() {
|
|
71
|
+
// 并行 flush,避免单个慢插件拖累整体
|
|
72
|
+
await Promise.all(this.startedPlugins.map(plugin => Promise.resolve().then(() => plugin.flush?.())));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public async shutdown() {
|
|
76
|
+
const plugins = [...this.startedPlugins].reverse();
|
|
77
|
+
for (const plugin of plugins) {
|
|
78
|
+
await plugin.shutdown?.();
|
|
79
|
+
}
|
|
80
|
+
this.startedPlugins = [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public async start(context: BkOTRuntimeContext) {
|
|
84
|
+
try {
|
|
85
|
+
for (const plugin of this.plugins) {
|
|
86
|
+
if (!isPluginEnabled(plugin, context.config)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
await plugin.init(context);
|
|
90
|
+
this.startedPlugins.push(plugin);
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
await this.shutdown();
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tencent is pleased to support the open source community by making
|
|
3
|
+
* 蓝鲸智云PaaS平台 (BlueKing PaaS) available.
|
|
4
|
+
*
|
|
5
|
+
* Copyright (C) 2017-2025 Tencent. All rights reserved.
|
|
6
|
+
*
|
|
7
|
+
* 蓝鲸智云PaaS平台 (BlueKing PaaS) is licensed under the MIT License.
|
|
8
|
+
*
|
|
9
|
+
* License for 蓝鲸智云PaaS平台 (BlueKing PaaS):
|
|
10
|
+
*
|
|
11
|
+
* ---------------------------------------------------
|
|
12
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
13
|
+
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
|
14
|
+
* the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
|
|
15
|
+
* to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
16
|
+
*
|
|
17
|
+
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of
|
|
18
|
+
* the Software.
|
|
19
|
+
*
|
|
20
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
|
|
21
|
+
* THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
23
|
+
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
24
|
+
* IN THE SOFTWARE.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { isBkOTEndpoint } from './url';
|
|
28
|
+
|
|
29
|
+
import type { NormalizedBkOTConfig } from './config';
|
|
30
|
+
import type { Context } from '@opentelemetry/api';
|
|
31
|
+
import type { ReadableSpan, Span, SpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
32
|
+
|
|
33
|
+
const SELF_TELEMETRY_URL_KEYS = ['http.url', 'url.full'];
|
|
34
|
+
|
|
35
|
+
const getSpanUrl = (span: ReadableSpan): string | undefined => {
|
|
36
|
+
for (const key of SELF_TELEMETRY_URL_KEYS) {
|
|
37
|
+
const value = span.attributes?.[key];
|
|
38
|
+
if (typeof value === 'string') {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 包装 SpanProcessor,在 onEnd 阶段过滤指向自身上报 endpoint 的 span,
|
|
47
|
+
* 兜底防止 official fetch/xhr 拦截器对 OTel 自身请求产生回环 span。
|
|
48
|
+
*/
|
|
49
|
+
export class FilteringSpanProcessor implements SpanProcessor {
|
|
50
|
+
public constructor(
|
|
51
|
+
private readonly inner: SpanProcessor,
|
|
52
|
+
private readonly config: NormalizedBkOTConfig,
|
|
53
|
+
) {}
|
|
54
|
+
|
|
55
|
+
public forceFlush(): Promise<void> {
|
|
56
|
+
return this.inner.forceFlush();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public onEnd(span: ReadableSpan): void {
|
|
60
|
+
const url = getSpanUrl(span);
|
|
61
|
+
if (url && isBkOTEndpoint(this.config, url)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.inner.onEnd(span);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public onStart(span: Span, parentContext: Context): void {
|
|
68
|
+
this.inner.onStart(span, parentContext);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public shutdown(): Promise<void> {
|
|
72
|
+
return this.inner.shutdown();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tencent is pleased to support the open source community by making
|
|
3
|
+
* 蓝鲸智云PaaS平台 (BlueKing PaaS) available.
|
|
4
|
+
*
|
|
5
|
+
* Copyright (C) 2017-2025 Tencent. All rights reserved.
|
|
6
|
+
*
|
|
7
|
+
* 蓝鲸智云PaaS平台 (BlueKing PaaS) is licensed under the MIT License.
|
|
8
|
+
*
|
|
9
|
+
* License for 蓝鲸智云PaaS平台 (BlueKing PaaS):
|
|
10
|
+
*
|
|
11
|
+
* ---------------------------------------------------
|
|
12
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
13
|
+
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
|
14
|
+
* the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
|
|
15
|
+
* to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
16
|
+
*
|
|
17
|
+
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of
|
|
18
|
+
* the Software.
|
|
19
|
+
*
|
|
20
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
|
|
21
|
+
* THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
23
|
+
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
24
|
+
* IN THE SOFTWARE.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export interface RouteChangeEvent {
|
|
28
|
+
fromUrl: string;
|
|
29
|
+
source: RouteChangeSource;
|
|
30
|
+
toUrl: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type RouteChangeSource = 'hashchange' | 'popstate' | 'pushState' | 'replaceState';
|
|
34
|
+
|
|
35
|
+
type RouteChangeHandler = (event: RouteChangeEvent) => void;
|
|
36
|
+
|
|
37
|
+
const subscribers = new Set<RouteChangeHandler>();
|
|
38
|
+
|
|
39
|
+
let lastUrl = '';
|
|
40
|
+
let originalPushState: typeof history.pushState | undefined;
|
|
41
|
+
let originalReplaceState: typeof history.replaceState | undefined;
|
|
42
|
+
let patched = false;
|
|
43
|
+
|
|
44
|
+
const getCurrentUrl = () => (typeof location === 'undefined' ? '' : location.href);
|
|
45
|
+
|
|
46
|
+
const emitRouteChange = (source: RouteChangeSource, fromUrl: string) => {
|
|
47
|
+
const toUrl = getCurrentUrl();
|
|
48
|
+
lastUrl = toUrl;
|
|
49
|
+
const event: RouteChangeEvent = { fromUrl, source, toUrl };
|
|
50
|
+
|
|
51
|
+
for (const subscriber of Array.from(subscribers)) {
|
|
52
|
+
subscriber(event);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const onPopState = () => {
|
|
57
|
+
emitRouteChange('popstate', lastUrl || getCurrentUrl());
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const onHashChange = () => {
|
|
61
|
+
emitRouteChange('hashchange', lastUrl || getCurrentUrl());
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const patchHistory = () => {
|
|
65
|
+
if (patched || typeof window === 'undefined' || typeof history === 'undefined') {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
patched = true;
|
|
70
|
+
lastUrl = getCurrentUrl();
|
|
71
|
+
originalPushState = history.pushState;
|
|
72
|
+
originalReplaceState = history.replaceState;
|
|
73
|
+
|
|
74
|
+
history.pushState = function patchedPushState(this: History, data: any, title: string, url?: null | string | URL) {
|
|
75
|
+
const fromUrl = lastUrl || getCurrentUrl();
|
|
76
|
+
const result = originalPushState?.apply(this, [data, title, url]);
|
|
77
|
+
emitRouteChange('pushState', fromUrl);
|
|
78
|
+
return result;
|
|
79
|
+
};
|
|
80
|
+
history.replaceState = function patchedReplaceState(
|
|
81
|
+
this: History,
|
|
82
|
+
data: any,
|
|
83
|
+
title: string,
|
|
84
|
+
url?: null | string | URL,
|
|
85
|
+
) {
|
|
86
|
+
const fromUrl = lastUrl || getCurrentUrl();
|
|
87
|
+
const result = originalReplaceState?.apply(this, [data, title, url]);
|
|
88
|
+
emitRouteChange('replaceState', fromUrl);
|
|
89
|
+
return result;
|
|
90
|
+
};
|
|
91
|
+
window.addEventListener('popstate', onPopState);
|
|
92
|
+
window.addEventListener('hashchange', onHashChange);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const restoreHistory = () => {
|
|
96
|
+
if (!patched || typeof window === 'undefined' || typeof history === 'undefined') {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (originalPushState) {
|
|
101
|
+
history.pushState = originalPushState;
|
|
102
|
+
}
|
|
103
|
+
if (originalReplaceState) {
|
|
104
|
+
history.replaceState = originalReplaceState;
|
|
105
|
+
}
|
|
106
|
+
window.removeEventListener('popstate', onPopState);
|
|
107
|
+
window.removeEventListener('hashchange', onHashChange);
|
|
108
|
+
originalPushState = undefined;
|
|
109
|
+
originalReplaceState = undefined;
|
|
110
|
+
patched = false;
|
|
111
|
+
lastUrl = '';
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const subscribeRouteChange = (handler: RouteChangeHandler) => {
|
|
115
|
+
if (typeof window === 'undefined' || typeof history === 'undefined') {
|
|
116
|
+
return () => {};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
patchHistory();
|
|
120
|
+
subscribers.add(handler);
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
subscribers.delete(handler);
|
|
124
|
+
if (subscribers.size === 0) {
|
|
125
|
+
restoreHistory();
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
};
|