@dxos/observability 0.8.4-main.9be5663bfe → 0.8.4-main.abd8ff62ef
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/dist/lib/browser/{chunk-K4VFBKST.mjs → chunk-3534PNWM.mjs} +1 -1
- package/dist/lib/browser/{chunk-K4VFBKST.mjs.map → chunk-3534PNWM.mjs.map} +2 -2
- package/dist/lib/browser/index.mjs +111 -184
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/{log-processor-FDLTDQEM.mjs → log-processor-HPHWNBOK.mjs} +11 -18
- package/dist/lib/browser/log-processor-HPHWNBOK.mjs.map +7 -0
- package/dist/lib/browser/{logs-ATTRIUTL.mjs → logs-A2VDAHHD.mjs} +5 -3
- package/dist/lib/browser/{logs-ATTRIUTL.mjs.map → logs-A2VDAHHD.mjs.map} +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/{metrics-PKTV6IGF.mjs → metrics-6OSN54ZV.mjs} +5 -20
- package/dist/lib/browser/{metrics-PKTV6IGF.mjs.map → metrics-6OSN54ZV.mjs.map} +2 -2
- package/dist/lib/browser/traces-browser-6MRGAKFC.mjs +151 -0
- package/dist/lib/browser/traces-browser-6MRGAKFC.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-FEVP3MK4.mjs → chunk-SYPJQI4L.mjs} +1 -1
- package/dist/lib/node-esm/{chunk-FEVP3MK4.mjs.map → chunk-SYPJQI4L.mjs.map} +2 -2
- package/dist/lib/node-esm/index.mjs +100 -154
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/{log-processor-TKJVJJSJ.mjs → log-processor-ZODHERST.mjs} +11 -18
- package/dist/lib/node-esm/log-processor-ZODHERST.mjs.map +7 -0
- package/dist/lib/node-esm/{logs-7J45KLM7.mjs → logs-TA32MBMW.mjs} +5 -3
- package/dist/lib/node-esm/{logs-7J45KLM7.mjs.map → logs-TA32MBMW.mjs.map} +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/{metrics-H7DDLYSG.mjs → metrics-C5IKPAWD.mjs} +5 -20
- package/dist/lib/node-esm/{metrics-H7DDLYSG.mjs.map → metrics-C5IKPAWD.mjs.map} +2 -2
- package/dist/lib/node-esm/traces-5LKHMVVG.mjs +122 -0
- package/dist/lib/node-esm/traces-5LKHMVVG.mjs.map +7 -0
- package/dist/types/src/cli-observability-secrets.json +2 -2
- package/dist/types/src/extensions/otel/extension.d.ts.map +1 -1
- package/dist/types/src/extensions/otel/extension.js +83 -6
- package/dist/types/src/extensions/otel/extension.js.map +1 -1
- package/dist/types/src/extensions/otel/logs.d.ts.map +1 -1
- package/dist/types/src/extensions/otel/logs.js +4 -1
- package/dist/types/src/extensions/otel/logs.js.map +1 -1
- package/dist/types/src/extensions/otel/metrics.d.ts.map +1 -1
- package/dist/types/src/extensions/otel/metrics.js.map +1 -1
- package/dist/types/src/extensions/otel/otel.d.ts.map +1 -1
- package/dist/types/src/extensions/otel/span-processors.d.ts +25 -0
- package/dist/types/src/extensions/otel/span-processors.d.ts.map +1 -0
- package/dist/types/src/extensions/otel/span-processors.js +41 -0
- package/dist/types/src/extensions/otel/span-processors.js.map +1 -0
- package/dist/types/src/extensions/otel/traces-browser.d.ts +17 -0
- package/dist/types/src/extensions/otel/traces-browser.d.ts.map +1 -1
- package/dist/types/src/extensions/otel/traces-browser.js +53 -34
- package/dist/types/src/extensions/otel/traces-browser.js.map +1 -1
- package/dist/types/src/extensions/otel/traces.d.ts +17 -0
- package/dist/types/src/extensions/otel/traces.d.ts.map +1 -1
- package/dist/types/src/extensions/otel/traces.js +57 -8
- package/dist/types/src/extensions/otel/traces.js.map +1 -1
- package/dist/types/src/extensions/posthog/extension.d.ts +7 -3
- package/dist/types/src/extensions/posthog/extension.d.ts.map +1 -1
- package/dist/types/src/extensions/posthog/extension.js +7 -7
- package/dist/types/src/extensions/posthog/extension.js.map +1 -1
- package/dist/types/src/extensions/posthog/log-processor.d.ts.map +1 -1
- package/dist/types/src/extensions/posthog/log-processor.js +10 -19
- package/dist/types/src/extensions/posthog/log-processor.js.map +1 -1
- package/dist/types/src/extensions/posthog/log-processor.test.js +2 -2
- package/dist/types/src/extensions/posthog/log-processor.test.js.map +1 -1
- package/dist/types/src/observability.d.ts.map +1 -1
- package/dist/types/src/providers/client-observability.d.ts.map +1 -1
- package/dist/types/src/providers/client-observability.js +2 -0
- package/dist/types/src/providers/client-observability.js.map +1 -1
- package/dist/types/src/providers/ip-data.d.ts.map +1 -1
- package/dist/types/src/storage/browser.d.ts.map +1 -1
- package/dist/types/src/storage/browser.js +10 -0
- package/dist/types/src/storage/browser.js.map +1 -1
- package/dist/types/src/storage/node.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +19 -16
- package/src/cli-observability-secrets.json +2 -2
- package/src/extensions/otel/extension.ts +87 -7
- package/src/extensions/otel/logs.ts +4 -1
- package/src/extensions/otel/span-processors.ts +45 -0
- package/src/extensions/otel/traces-browser.ts +68 -39
- package/src/extensions/otel/traces.ts +74 -10
- package/src/extensions/posthog/extension.ts +14 -9
- package/src/extensions/posthog/log-processor.test.ts +7 -6
- package/src/extensions/posthog/log-processor.ts +10 -21
- package/src/providers/client-observability.ts +2 -0
- package/src/storage/browser.ts +9 -0
- package/dist/lib/browser/log-processor-FDLTDQEM.mjs.map +0 -7
- package/dist/lib/browser/traces-browser-PUNCJ5KV.mjs +0 -112
- package/dist/lib/browser/traces-browser-PUNCJ5KV.mjs.map +0 -7
- package/dist/lib/node-esm/log-processor-TKJVJJSJ.mjs.map +0 -7
- package/dist/lib/node-esm/traces-KMTHMYFX.mjs +0 -44
- package/dist/lib/node-esm/traces-KMTHMYFX.mjs.map +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/observability",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.abd8ff62ef",
|
|
4
4
|
"description": "Provides a common interface for app and platform observability",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -49,25 +49,28 @@
|
|
|
49
49
|
"posthog-js": "^1.353.0",
|
|
50
50
|
"uuid": "^9.0.0",
|
|
51
51
|
"zone.js": "^0.15.0",
|
|
52
|
-
"@dxos/async": "0.8.4-main.
|
|
53
|
-
"@dxos/client": "0.8.4-main.
|
|
54
|
-
"@dxos/client-protocol": "0.8.4-main.
|
|
55
|
-
"@dxos/client-services": "0.8.4-main.
|
|
56
|
-
"@dxos/
|
|
57
|
-
"@dxos/context": "0.8.4-main.
|
|
58
|
-
"@dxos/
|
|
59
|
-
"@dxos/
|
|
60
|
-
"@dxos/
|
|
61
|
-
"@dxos/
|
|
62
|
-
"@dxos/
|
|
63
|
-
"@dxos/
|
|
64
|
-
"@dxos/
|
|
65
|
-
"@dxos/
|
|
52
|
+
"@dxos/async": "0.8.4-main.abd8ff62ef",
|
|
53
|
+
"@dxos/client": "0.8.4-main.abd8ff62ef",
|
|
54
|
+
"@dxos/client-protocol": "0.8.4-main.abd8ff62ef",
|
|
55
|
+
"@dxos/client-services": "0.8.4-main.abd8ff62ef",
|
|
56
|
+
"@dxos/config": "0.8.4-main.abd8ff62ef",
|
|
57
|
+
"@dxos/context": "0.8.4-main.abd8ff62ef",
|
|
58
|
+
"@dxos/debug": "0.8.4-main.abd8ff62ef",
|
|
59
|
+
"@dxos/invariant": "0.8.4-main.abd8ff62ef",
|
|
60
|
+
"@dxos/log-store-idb": "0.8.4-main.abd8ff62ef",
|
|
61
|
+
"@dxos/log": "0.8.4-main.abd8ff62ef",
|
|
62
|
+
"@dxos/node-std": "0.8.4-main.abd8ff62ef",
|
|
63
|
+
"@dxos/network-manager": "0.8.4-main.abd8ff62ef",
|
|
64
|
+
"@dxos/protocols": "0.8.4-main.abd8ff62ef",
|
|
65
|
+
"@dxos/tracing": "0.8.4-main.abd8ff62ef",
|
|
66
|
+
"@dxos/util": "0.8.4-main.abd8ff62ef"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
69
|
"@effect/platform": "0.94.4",
|
|
69
70
|
"@types/debug": "^4.1.10",
|
|
70
|
-
"@types/js-yaml": "^4.0.5"
|
|
71
|
+
"@types/js-yaml": "^4.0.5",
|
|
72
|
+
"@dxos/effect": "0.8.4-main.abd8ff62ef",
|
|
73
|
+
"@dxos/messaging": "0.8.4-main.abd8ff62ef"
|
|
71
74
|
},
|
|
72
75
|
"peerDependencies": {
|
|
73
76
|
"@effect/platform": "0.94.4",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"POSTHOG_API_KEY": null,
|
|
3
3
|
"IPDATA_API_KEY": "73dfdecdf979c18f07d50cf841bbdd9e589f237256326ac8cca23786",
|
|
4
|
-
"OTEL_ENDPOINT": "
|
|
5
|
-
"OTEL_HEADERS":
|
|
4
|
+
"OTEL_ENDPOINT": "/api/otel",
|
|
5
|
+
"OTEL_HEADERS": null
|
|
6
6
|
}
|
|
@@ -9,7 +9,7 @@ import * as Match from 'effect/Match';
|
|
|
9
9
|
import * as Option from 'effect/Option';
|
|
10
10
|
import * as Ref from 'effect/Ref';
|
|
11
11
|
|
|
12
|
-
import { type Config } from '@dxos/config';
|
|
12
|
+
import { type Config, resolveTelemetryTag } from '@dxos/config';
|
|
13
13
|
import { LogLevel, log } from '@dxos/log';
|
|
14
14
|
import { isNode, isNonNullable } from '@dxos/util';
|
|
15
15
|
|
|
@@ -57,7 +57,8 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
|
|
|
57
57
|
const { OtelTraces } = yield* Effect.promise(() => import('./traces'));
|
|
58
58
|
|
|
59
59
|
const cachedDisabled = yield* Effect.promise(() => isObservabilityDisabled(serviceName));
|
|
60
|
-
const
|
|
60
|
+
const disabled = cachedDisabled || isObservabilityDisabledSync(serviceName);
|
|
61
|
+
const enabledRef = yield* Ref.make(!disabled);
|
|
61
62
|
const tags = new Map<string, string>();
|
|
62
63
|
|
|
63
64
|
const endpoint = isNode()
|
|
@@ -78,11 +79,31 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
|
|
|
78
79
|
return stubExtension;
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
// Matches edge's `ctx.tag` span attribute (stamped by the edge log middleware
|
|
83
|
+
// when it reads the `X-DXOS-Client-Tag` header, see
|
|
84
|
+
// /edge/packages/services/edge/src/log-middleware.ts).
|
|
85
|
+
//
|
|
86
|
+
// Stamped in TWO places:
|
|
87
|
+
// 1. Resource attribute — so logs and metrics emitted from this extension
|
|
88
|
+
// also carry it, and for zero per-span cost.
|
|
89
|
+
// 2. Span attribute via `tags` — because edge puts `ctx.tag` in the span-
|
|
90
|
+
// attribute context (not resource), and SigNoz indexes the two contexts
|
|
91
|
+
// separately. Stamping it on span attributes here keeps a single
|
|
92
|
+
// `ctx.tag = <value>` filter matching spans from both tiers in SigNoz
|
|
93
|
+
// without requiring qualified `attribute.ctx.tag`/`resource.ctx.tag` syntax.
|
|
94
|
+
const clientTag = resolveTelemetryTag(config);
|
|
95
|
+
if (clientTag) {
|
|
96
|
+
tags.set('ctx.tag', clientTag);
|
|
97
|
+
}
|
|
98
|
+
|
|
81
99
|
const resource = defaultResource().merge(
|
|
82
100
|
resourceFromAttributes({
|
|
83
101
|
[ATTR_SERVICE_NAME]: serviceName,
|
|
84
102
|
[ATTR_SERVICE_VERSION]: serviceVersion,
|
|
103
|
+
'session.id': crypto.randomUUID(),
|
|
85
104
|
'deployment.environment': environment,
|
|
105
|
+
'dxos.process.type': detectProcessType(),
|
|
106
|
+
...(clientTag ? { 'ctx.tag': clientTag } : {}),
|
|
86
107
|
}),
|
|
87
108
|
);
|
|
88
109
|
|
|
@@ -114,9 +135,13 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
|
|
|
114
135
|
})
|
|
115
136
|
: undefined;
|
|
116
137
|
|
|
117
|
-
|
|
138
|
+
const extension: Extension = {
|
|
118
139
|
initialize: () =>
|
|
119
140
|
Effect.sync(() => {
|
|
141
|
+
if (disabled) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
120
145
|
if (logs) {
|
|
121
146
|
log.runtimeConfig.processors.push(logs.logProcessor);
|
|
122
147
|
}
|
|
@@ -134,13 +159,31 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
|
|
|
134
159
|
}),
|
|
135
160
|
close: () =>
|
|
136
161
|
Effect.promise(async () => {
|
|
137
|
-
|
|
138
|
-
|
|
162
|
+
// Run logs/metrics close concurrently and swallow their failures so the
|
|
163
|
+
// tracer provider shutdown below ALWAYS runs. Without this, a rejection
|
|
164
|
+
// from logs or metrics would drop the tracer provider's BatchSpanProcessor
|
|
165
|
+
// queue on process exit, manifesting as "Missing Span" in SigNoz for any
|
|
166
|
+
// already-exported children.
|
|
167
|
+
const results = await Promise.allSettled([logs?.close(), metrics?.close()]);
|
|
168
|
+
for (const result of results) {
|
|
169
|
+
if (result.status === 'rejected') {
|
|
170
|
+
log.catch(result.reason);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Critical: shut down the tracer provider so BatchSpanProcessor drains its
|
|
174
|
+
// queue. Otherwise spans enqueued in the last 5s (close/teardown spans)
|
|
175
|
+
// are dropped on process exit.
|
|
176
|
+
await traces?.close();
|
|
139
177
|
}),
|
|
140
178
|
flush: () =>
|
|
141
179
|
Effect.promise(async () => {
|
|
142
|
-
await logs?.flush();
|
|
143
|
-
|
|
180
|
+
const results = await Promise.allSettled([logs?.flush(), metrics?.flush()]);
|
|
181
|
+
for (const result of results) {
|
|
182
|
+
if (result.status === 'rejected') {
|
|
183
|
+
log.catch(result.reason);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
await traces?.flush();
|
|
144
187
|
}),
|
|
145
188
|
setTags: (incomingTags) => {
|
|
146
189
|
for (const [key, value] of Object.entries(incomingTags)) {
|
|
@@ -164,8 +207,45 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
|
|
|
164
207
|
traces ? ({ kind: 'traces', isAvailable: () => Effect.succeed(true) } satisfies ExtensionApi) : undefined,
|
|
165
208
|
].filter(isNonNullable),
|
|
166
209
|
};
|
|
210
|
+
|
|
211
|
+
return extension;
|
|
167
212
|
});
|
|
168
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Synchronous best-effort check for observability opt-out.
|
|
216
|
+
* Prevents telemetry from being emitted before the async storage check completes.
|
|
217
|
+
*/
|
|
218
|
+
const isObservabilityDisabledSync = (serviceName: string): boolean => {
|
|
219
|
+
if (isNode()) {
|
|
220
|
+
return process.env.DX_DISABLE_OBSERVABILITY === 'true';
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
if (typeof localStorage !== 'undefined') {
|
|
224
|
+
return localStorage.getItem(`${serviceName}/observability-disabled`) === 'true';
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
// localStorage not available (e.g., in workers).
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/** Best-effort detection of the JavaScript execution context type. */
|
|
233
|
+
const detectProcessType = (): string => {
|
|
234
|
+
if (isNode()) {
|
|
235
|
+
return 'node';
|
|
236
|
+
}
|
|
237
|
+
if (typeof window !== 'undefined') {
|
|
238
|
+
return 'browser';
|
|
239
|
+
}
|
|
240
|
+
if (typeof (globalThis as any).ServiceWorkerGlobalScope !== 'undefined') {
|
|
241
|
+
return 'service-worker';
|
|
242
|
+
}
|
|
243
|
+
if (typeof (globalThis as any).SharedWorkerGlobalScope !== 'undefined') {
|
|
244
|
+
return 'shared-worker';
|
|
245
|
+
}
|
|
246
|
+
return 'dedicated-worker';
|
|
247
|
+
};
|
|
248
|
+
|
|
169
249
|
const parseHeaders = (unparsedHeaders: string): Record<string, string> => {
|
|
170
250
|
return unparsedHeaders.split(';').reduce((acc: Record<string, string>, header) => {
|
|
171
251
|
const [key, ...rest] = header.split(':');
|
|
@@ -81,8 +81,11 @@ export class OtelLogs {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/** Maps {@link LogLevel} to OpenTelemetry {@link SeverityNumber}. */
|
|
84
85
|
const convertLevel = (level: LogLevel): SeverityNumber => {
|
|
85
86
|
switch (level) {
|
|
87
|
+
case LogLevel.TRACE:
|
|
88
|
+
return SeverityNumber.TRACE;
|
|
86
89
|
case LogLevel.DEBUG:
|
|
87
90
|
return SeverityNumber.DEBUG;
|
|
88
91
|
case LogLevel.VERBOSE:
|
|
@@ -94,7 +97,7 @@ const convertLevel = (level: LogLevel): SeverityNumber => {
|
|
|
94
97
|
case LogLevel.ERROR:
|
|
95
98
|
return SeverityNumber.ERROR;
|
|
96
99
|
default:
|
|
97
|
-
return SeverityNumber.
|
|
100
|
+
return SeverityNumber.UNSPECIFIED;
|
|
98
101
|
}
|
|
99
102
|
};
|
|
100
103
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Context } from '@opentelemetry/api';
|
|
6
|
+
import { type ReadableSpan, type Span, type SpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
7
|
+
|
|
8
|
+
import { log } from '@dxos/log';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Injects dynamic tags as attributes on every span.
|
|
12
|
+
*
|
|
13
|
+
* This is an `onStart`-only processor; `onEnd`/`shutdown`/`forceFlush` are no-ops
|
|
14
|
+
* because this processor never holds pending work (delegates export to downstream
|
|
15
|
+
* processors like BatchSpanProcessor).
|
|
16
|
+
*
|
|
17
|
+
* Use for tags that are not known at provider construction (e.g. identity
|
|
18
|
+
* resolved asynchronously after login). For values known at startup, prefer
|
|
19
|
+
* putting them on the `Resource` — see `extension.ts`.
|
|
20
|
+
*
|
|
21
|
+
* Failures in the tag provider are swallowed so they can never break span
|
|
22
|
+
* recording (the processor is best-effort and runs on the startSpan hot path).
|
|
23
|
+
*/
|
|
24
|
+
export class TagInjectorSpanProcessor implements SpanProcessor {
|
|
25
|
+
constructor(private readonly _getTags: () => Record<string, string>) {}
|
|
26
|
+
|
|
27
|
+
onStart(span: Span, _parentContext: Context): void {
|
|
28
|
+
let tags: Record<string, string>;
|
|
29
|
+
try {
|
|
30
|
+
tags = this._getTags();
|
|
31
|
+
} catch (err) {
|
|
32
|
+
log.catch(err);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
36
|
+
span.setAttribute(key, value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onEnd(_span: ReadableSpan): void {}
|
|
41
|
+
|
|
42
|
+
async shutdown(): Promise<void> {}
|
|
43
|
+
|
|
44
|
+
async forceFlush(): Promise<void> {}
|
|
45
|
+
}
|
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ROOT_CONTEXT,
|
|
7
|
+
SpanStatusCode,
|
|
8
|
+
type Tracer,
|
|
9
|
+
context as otelContext,
|
|
10
|
+
propagation,
|
|
11
|
+
trace,
|
|
12
|
+
} from '@opentelemetry/api';
|
|
6
13
|
import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
|
|
7
14
|
import { W3CTraceContextPropagator } from '@opentelemetry/core';
|
|
8
15
|
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
@@ -11,47 +18,27 @@ import {
|
|
|
11
18
|
AlwaysOnSampler,
|
|
12
19
|
BatchSpanProcessor,
|
|
13
20
|
ParentBasedSampler,
|
|
14
|
-
type ReadableSpan,
|
|
15
|
-
type SpanProcessor,
|
|
16
21
|
TraceIdRatioBasedSampler,
|
|
17
22
|
} from '@opentelemetry/sdk-trace-base';
|
|
18
23
|
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
|
|
19
24
|
import { ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
|
|
20
25
|
|
|
21
26
|
import { log } from '@dxos/log';
|
|
22
|
-
import { type StartSpanOptions, TRACE_PROCESSOR } from '@dxos/tracing';
|
|
27
|
+
import { TRACE_ALL_KEY, type RemoteSpan, type StartSpanOptions, TRACE_PROCESSOR } from '@dxos/tracing';
|
|
23
28
|
|
|
24
29
|
import { type OtelOptions } from './otel';
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Injects dynamic tags (e.g. userId) as attributes on every span.
|
|
28
|
-
*/
|
|
29
|
-
class TagInjectorSpanProcessor implements SpanProcessor {
|
|
30
|
-
constructor(private readonly _getTags: () => Record<string, string>) {}
|
|
31
|
-
|
|
32
|
-
onStart(span: { setAttribute: (key: string, value: string) => void }): void {
|
|
33
|
-
const tags = this._getTags();
|
|
34
|
-
for (const [key, value] of Object.entries(tags)) {
|
|
35
|
-
span.setAttribute(key, value);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
onEnd(_span: ReadableSpan): void {}
|
|
40
|
-
|
|
41
|
-
async shutdown(): Promise<void> {}
|
|
42
|
-
|
|
43
|
-
async forceFlush(): Promise<void> {}
|
|
44
|
-
}
|
|
30
|
+
import { TagInjectorSpanProcessor } from './span-processors';
|
|
45
31
|
|
|
46
32
|
export class OtelTraces {
|
|
47
33
|
private _tracer: Tracer;
|
|
34
|
+
private readonly _tracerProvider: WebTracerProvider;
|
|
48
35
|
|
|
49
36
|
constructor(private readonly options: OtelOptions) {
|
|
50
37
|
propagation.setGlobalPropagator(new W3CTraceContextPropagator());
|
|
51
38
|
|
|
52
|
-
const forceTraceAll = typeof localStorage !== 'undefined' && localStorage.getItem(
|
|
39
|
+
const forceTraceAll = typeof localStorage !== 'undefined' && localStorage.getItem(TRACE_ALL_KEY) === 'true';
|
|
53
40
|
|
|
54
|
-
|
|
41
|
+
this._tracerProvider = new WebTracerProvider({
|
|
55
42
|
resource: this.options.resource,
|
|
56
43
|
sampler: new ParentBasedSampler({
|
|
57
44
|
root: forceTraceAll ? new AlwaysOnSampler() : new TraceIdRatioBasedSampler(0.3),
|
|
@@ -69,7 +56,7 @@ export class OtelTraces {
|
|
|
69
56
|
],
|
|
70
57
|
});
|
|
71
58
|
|
|
72
|
-
trace.setGlobalTracerProvider(
|
|
59
|
+
trace.setGlobalTracerProvider(this._tracerProvider);
|
|
73
60
|
|
|
74
61
|
this._tracer = trace.getTracer(
|
|
75
62
|
'dxos-observability',
|
|
@@ -77,34 +64,76 @@ export class OtelTraces {
|
|
|
77
64
|
);
|
|
78
65
|
}
|
|
79
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Forcibly flush the BatchSpanProcessor. Call before process exit / page unload
|
|
69
|
+
* to avoid losing queued spans (which manifests as "Missing Span" in SigNoz —
|
|
70
|
+
* their already-exported children reference a parent that never made it to OTLP).
|
|
71
|
+
*/
|
|
72
|
+
public async flush(): Promise<void> {
|
|
73
|
+
await this._tracerProvider.forceFlush();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Flush + shut down the tracer provider via `WebTracerProvider.shutdown()`,
|
|
78
|
+
* which forces a final export then terminates all span processors.
|
|
79
|
+
*
|
|
80
|
+
* Terminal and effectively one-shot: safe to call after `flush()`, but
|
|
81
|
+
* `flush()` MUST NOT be called after `close()` — shutdown stops further
|
|
82
|
+
* exporting, so subsequent `close()`/`flush()` calls resolve without
|
|
83
|
+
* emitting new spans.
|
|
84
|
+
*/
|
|
85
|
+
public async close(): Promise<void> {
|
|
86
|
+
await this._tracerProvider.shutdown();
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
public start(): void {
|
|
81
90
|
log('trace processor registered');
|
|
82
91
|
|
|
83
92
|
registerInstrumentations({
|
|
84
93
|
instrumentations: [
|
|
85
94
|
getWebAutoInstrumentations({
|
|
86
|
-
// Fetch is disabled in favor of explicit ctx on our call sites; if re-enabled, keep noisy
|
|
87
|
-
// third-party URLs out of traces (matches prior browser tracing setup).
|
|
88
95
|
'@opentelemetry/instrumentation-fetch': { enabled: false, ignoreUrls: [/api\.ipdata\.co/] },
|
|
89
96
|
'@opentelemetry/instrumentation-document-load': { enabled: false },
|
|
90
97
|
'@opentelemetry/instrumentation-xml-http-request': { enabled: false },
|
|
98
|
+
'@opentelemetry/instrumentation-user-interaction': { enabled: false },
|
|
91
99
|
}),
|
|
92
100
|
],
|
|
93
101
|
});
|
|
94
102
|
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
const tracer = this._tracer;
|
|
104
|
+
|
|
105
|
+
TRACE_PROCESSOR.tracingBackend = {
|
|
106
|
+
startSpan: (options: StartSpanOptions): RemoteSpan => {
|
|
97
107
|
log('begin otel trace', { options });
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
const parentCtx = options.parentContext
|
|
109
|
+
? propagation.extract(ROOT_CONTEXT, {
|
|
110
|
+
traceparent: options.parentContext.traceparent,
|
|
111
|
+
tracestate: options.parentContext.tracestate ?? '',
|
|
112
|
+
})
|
|
113
|
+
: otelContext.active();
|
|
114
|
+
|
|
115
|
+
const span = tracer.startSpan(options.name, options, parentCtx);
|
|
116
|
+
|
|
117
|
+
const sc = span.spanContext();
|
|
118
|
+
const spanContext =
|
|
119
|
+
sc && sc.traceId && sc.spanId
|
|
120
|
+
? {
|
|
121
|
+
traceparent: `00-${sc.traceId}-${sc.spanId}-${(sc.traceFlags ?? 0).toString(16).padStart(2, '0')}`,
|
|
122
|
+
tracestate: sc.traceState?.serialize(),
|
|
123
|
+
}
|
|
124
|
+
: undefined;
|
|
125
|
+
|
|
102
126
|
return {
|
|
103
|
-
end: () => span.end(),
|
|
104
|
-
|
|
105
|
-
|
|
127
|
+
end: (endTime?: number) => span.end(endTime),
|
|
128
|
+
setError: (err: unknown) => {
|
|
129
|
+
if (err instanceof Error) {
|
|
130
|
+
span.recordException(err);
|
|
131
|
+
}
|
|
132
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err instanceof Error ? err.message : String(err) });
|
|
133
|
+
},
|
|
134
|
+
spanContext,
|
|
106
135
|
};
|
|
107
136
|
},
|
|
108
|
-
}
|
|
137
|
+
};
|
|
109
138
|
}
|
|
110
139
|
}
|
|
@@ -2,48 +2,112 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ROOT_CONTEXT,
|
|
7
|
+
SpanStatusCode,
|
|
8
|
+
type Tracer,
|
|
9
|
+
context as otelContext,
|
|
10
|
+
propagation,
|
|
11
|
+
trace,
|
|
12
|
+
} from '@opentelemetry/api';
|
|
13
|
+
import { W3CTraceContextPropagator } from '@opentelemetry/core';
|
|
6
14
|
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
7
15
|
import { BasicTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
8
16
|
import { ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
|
|
9
|
-
import { log } from 'debug';
|
|
10
17
|
|
|
11
|
-
import {
|
|
18
|
+
import { log } from '@dxos/log';
|
|
19
|
+
import { type RemoteSpan, type StartSpanOptions, TRACE_PROCESSOR } from '@dxos/tracing';
|
|
12
20
|
|
|
13
21
|
import { type OtelOptions } from './otel';
|
|
22
|
+
import { TagInjectorSpanProcessor } from './span-processors';
|
|
14
23
|
|
|
15
24
|
export class OtelTraces {
|
|
16
25
|
private _tracer: Tracer;
|
|
26
|
+
private readonly _tracerProvider: BasicTracerProvider;
|
|
17
27
|
|
|
18
28
|
constructor(private readonly options: OtelOptions) {
|
|
19
|
-
|
|
29
|
+
propagation.setGlobalPropagator(new W3CTraceContextPropagator());
|
|
30
|
+
|
|
31
|
+
this._tracerProvider = new BasicTracerProvider({
|
|
20
32
|
resource: this.options.resource,
|
|
21
33
|
spanProcessors: [
|
|
34
|
+
new TagInjectorSpanProcessor(this.options.getTags),
|
|
22
35
|
new BatchSpanProcessor(
|
|
23
36
|
new OTLPTraceExporter({
|
|
24
37
|
url: this.options.endpoint + '/v1/traces',
|
|
25
38
|
headers: this.options.headers,
|
|
26
|
-
concurrencyLimit: 10,
|
|
39
|
+
concurrencyLimit: 10,
|
|
27
40
|
}),
|
|
28
41
|
),
|
|
29
42
|
],
|
|
30
43
|
});
|
|
31
44
|
|
|
32
|
-
trace.setGlobalTracerProvider(
|
|
45
|
+
trace.setGlobalTracerProvider(this._tracerProvider);
|
|
33
46
|
this._tracer = trace.getTracer(
|
|
34
47
|
'dxos-observability',
|
|
35
48
|
this.options.resource.attributes[ATTR_SERVICE_VERSION]?.toString(),
|
|
36
49
|
);
|
|
37
50
|
}
|
|
38
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Forcibly flush the BatchSpanProcessor. Call before process exit to avoid
|
|
54
|
+
* losing queued spans (which manifests as "Missing Span" in SigNoz — their
|
|
55
|
+
* already-exported children reference a parent that never made it to OTLP).
|
|
56
|
+
*/
|
|
57
|
+
public async flush(): Promise<void> {
|
|
58
|
+
await this._tracerProvider.forceFlush();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Flush + shut down the tracer provider via `BasicTracerProvider.shutdown()`,
|
|
63
|
+
* which forces a final export then terminates all span processors.
|
|
64
|
+
*
|
|
65
|
+
* Terminal and effectively one-shot: safe to call after `flush()`, but
|
|
66
|
+
* `flush()` MUST NOT be called after `close()` — shutdown stops further
|
|
67
|
+
* exporting, so subsequent `close()`/`flush()` calls resolve without
|
|
68
|
+
* emitting new spans.
|
|
69
|
+
*/
|
|
70
|
+
public async close(): Promise<void> {
|
|
71
|
+
await this._tracerProvider.shutdown();
|
|
72
|
+
}
|
|
73
|
+
|
|
39
74
|
public start(): void {
|
|
40
75
|
log('trace processor registered');
|
|
41
76
|
|
|
42
|
-
|
|
43
|
-
|
|
77
|
+
const tracer = this._tracer;
|
|
78
|
+
|
|
79
|
+
TRACE_PROCESSOR.tracingBackend = {
|
|
80
|
+
startSpan: (options: StartSpanOptions): RemoteSpan => {
|
|
44
81
|
log('begin otel trace', { options });
|
|
45
|
-
|
|
82
|
+
const parentCtx = options.parentContext
|
|
83
|
+
? propagation.extract(ROOT_CONTEXT, {
|
|
84
|
+
traceparent: options.parentContext.traceparent,
|
|
85
|
+
tracestate: options.parentContext.tracestate ?? '',
|
|
86
|
+
})
|
|
87
|
+
: otelContext.active();
|
|
88
|
+
|
|
89
|
+
const span = tracer.startSpan(options.name, options, parentCtx);
|
|
90
|
+
|
|
91
|
+
const sc = span.spanContext();
|
|
92
|
+
const spanContext =
|
|
93
|
+
sc && sc.traceId && sc.spanId
|
|
94
|
+
? {
|
|
95
|
+
traceparent: `00-${sc.traceId}-${sc.spanId}-${(sc.traceFlags ?? 0).toString(16).padStart(2, '0')}`,
|
|
96
|
+
tracestate: sc.traceState?.serialize(),
|
|
97
|
+
}
|
|
98
|
+
: undefined;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
end: (endTime?: number) => span.end(endTime),
|
|
102
|
+
setError: (err: unknown) => {
|
|
103
|
+
if (err instanceof Error) {
|
|
104
|
+
span.recordException(err);
|
|
105
|
+
}
|
|
106
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err instanceof Error ? err.message : String(err) });
|
|
107
|
+
},
|
|
108
|
+
spanContext,
|
|
109
|
+
};
|
|
46
110
|
},
|
|
47
|
-
}
|
|
111
|
+
};
|
|
48
112
|
}
|
|
49
113
|
}
|
|
@@ -6,7 +6,8 @@ import * as Effect from 'effect/Effect';
|
|
|
6
6
|
import { type PostHogConfig } from 'posthog-js';
|
|
7
7
|
|
|
8
8
|
import { type Config } from '@dxos/config';
|
|
9
|
-
import {
|
|
9
|
+
import { log } from '@dxos/log';
|
|
10
|
+
import { type IdbLogStore } from '@dxos/log-store-idb';
|
|
10
11
|
|
|
11
12
|
import { type Extension } from '../../observability-extension';
|
|
12
13
|
import { stubExtension } from '../stub';
|
|
@@ -18,8 +19,12 @@ export type ExtensionsOptions = {
|
|
|
18
19
|
/** Deployment environment, e.g. `production` or `staging`. */
|
|
19
20
|
environment?: string;
|
|
20
21
|
posthog?: Partial<PostHogConfig>;
|
|
21
|
-
/**
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Shared persistent log store for debug log dumps.
|
|
24
|
+
* The owning app is expected to register `logStore.processor` with `log` itself —
|
|
25
|
+
* this extension only consumes the buffered logs (via `export()`).
|
|
26
|
+
*/
|
|
27
|
+
logStore?: IdbLogStore;
|
|
23
28
|
};
|
|
24
29
|
|
|
25
30
|
/** Upload serialized logs to the feedback-logs endpoint. Returns the R2 key on success. */
|
|
@@ -48,7 +53,7 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
|
|
|
48
53
|
release,
|
|
49
54
|
environment,
|
|
50
55
|
posthog: posthogConfig,
|
|
51
|
-
|
|
56
|
+
logStore,
|
|
52
57
|
}) {
|
|
53
58
|
if (typeof window === 'undefined') {
|
|
54
59
|
log('PostHog is being stubbed because it is running in a worker.');
|
|
@@ -65,7 +70,6 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
|
|
|
65
70
|
|
|
66
71
|
const { default: posthog } = yield* Effect.promise(() => import('posthog-js'));
|
|
67
72
|
const { logProcessor } = yield* Effect.promise(() => import('./log-processor'));
|
|
68
|
-
const logBuffer = externalLogBuffer ?? new LogBuffer();
|
|
69
73
|
let feedbackSurveyAvailable: boolean | null = null;
|
|
70
74
|
let unregisterPosthogProcessors: (() => void) | undefined;
|
|
71
75
|
|
|
@@ -103,10 +107,8 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
|
|
|
103
107
|
}
|
|
104
108
|
unregisterPosthogProcessors?.();
|
|
105
109
|
const removePosthogLog = log.addProcessor(logProcessor);
|
|
106
|
-
const removeLogBuffer = log.addProcessor(logBuffer.logProcessor);
|
|
107
110
|
unregisterPosthogProcessors = () => {
|
|
108
111
|
removePosthogLog();
|
|
109
|
-
removeLogBuffer();
|
|
110
112
|
};
|
|
111
113
|
}),
|
|
112
114
|
close: () =>
|
|
@@ -155,8 +157,11 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
|
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
let debugLogDumpKey: string | null = null;
|
|
158
|
-
if (form.includeLogs !== false &&
|
|
159
|
-
|
|
160
|
+
if (form.includeLogs !== false && logStore !== undefined) {
|
|
161
|
+
const ndjson = await logStore.export();
|
|
162
|
+
if (ndjson.length > 0) {
|
|
163
|
+
debugLogDumpKey = (await uploadLogs(ndjson)) ?? 'failed';
|
|
164
|
+
}
|
|
160
165
|
}
|
|
161
166
|
|
|
162
167
|
// https://posthog.com/docs/surveys/implementing-custom-surveys
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { InvariantViolation } from '@dxos/invariant';
|
|
8
|
-
import { type LogConfig, type
|
|
8
|
+
import { type LogConfig, LogEntry, type LogEntryInit, LogLevel } from '@dxos/log';
|
|
9
9
|
|
|
10
10
|
// Replace the posthog-js module with a stub so log-processor's top-level import resolves to our mock.
|
|
11
11
|
// Dynamic imports are used so that both the mock and log-processor are loaded after vi.mock is hoisted.
|
|
@@ -23,11 +23,12 @@ const baseConfig: LogConfig = {
|
|
|
23
23
|
processors: [],
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
const createEntry = (overrides: Partial<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
const createEntry = (overrides: Partial<LogEntryInit> = {}): LogEntry =>
|
|
27
|
+
new LogEntry({
|
|
28
|
+
level: LogLevel.ERROR,
|
|
29
|
+
message: 'test error',
|
|
30
|
+
...overrides,
|
|
31
|
+
});
|
|
31
32
|
|
|
32
33
|
describe('logProcessor', () => {
|
|
33
34
|
beforeEach(() => {
|