@dxos/observability 0.8.4-main.ef1bc66f44 → 0.8.4-main.f466a3d56e
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/LICENSE +102 -5
- package/README.md +14 -11
- package/dist/lib/browser/{chunk-K4VFBKST.mjs → chunk-PSFTIG54.mjs} +9 -2
- package/dist/lib/browser/chunk-PSFTIG54.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +181 -283
- package/dist/lib/browser/index.mjs.map +4 -4
- 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-UTNIFYHF.mjs} +7 -4
- package/dist/lib/browser/logs-UTNIFYHF.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/{metrics-PKTV6IGF.mjs → metrics-PRGSYAZJ.mjs} +7 -21
- package/dist/lib/browser/metrics-PRGSYAZJ.mjs.map +7 -0
- package/dist/lib/browser/traces-browser-XRINKQUA.mjs +154 -0
- package/dist/lib/browser/traces-browser-XRINKQUA.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-FEVP3MK4.mjs → chunk-EDDZWPYV.mjs} +9 -2
- package/dist/lib/node-esm/chunk-EDDZWPYV.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +166 -250
- package/dist/lib/node-esm/index.mjs.map +4 -4
- 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-ARJUVN3T.mjs} +7 -4
- package/dist/lib/node-esm/logs-ARJUVN3T.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/{metrics-H7DDLYSG.mjs → metrics-DREJOOAC.mjs} +7 -21
- package/dist/lib/node-esm/metrics-DREJOOAC.mjs.map +7 -0
- package/dist/lib/node-esm/traces-74F7JUKF.mjs +125 -0
- package/dist/lib/node-esm/traces-74F7JUKF.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 +112 -16
- 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 +6 -3
- 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 +2 -2
- package/dist/types/src/extensions/otel/metrics.js.map +1 -1
- package/dist/types/src/extensions/otel/otel.d.ts +7 -0
- package/dist/types/src/extensions/otel/otel.d.ts.map +1 -1
- package/dist/types/src/extensions/otel/otel.js +12 -0
- package/dist/types/src/extensions/otel/otel.js.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 +73 -12
- 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 +59 -9
- package/dist/types/src/extensions/otel/traces.js.map +1 -1
- package/dist/types/src/extensions/posthog/extension.d.ts +13 -0
- package/dist/types/src/extensions/posthog/extension.d.ts.map +1 -1
- package/dist/types/src/extensions/posthog/extension.js +41 -28
- 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/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/index.js +0 -1
- package/dist/types/src/index.js.map +1 -1
- package/dist/types/src/observability-extension.d.ts +1 -1
- package/dist/types/src/observability-extension.d.ts.map +1 -1
- package/dist/types/src/observability.d.ts.map +1 -1
- package/dist/types/src/observability.js +4 -2
- package/dist/types/src/observability.js.map +1 -1
- package/dist/types/src/observability.test.js +1 -1
- package/dist/types/src/observability.test.js.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/providers/ip-data.js +4 -2
- package/dist/types/src/providers/ip-data.js.map +1 -1
- package/dist/types/src/storage/browser.d.ts.map +1 -1
- package/dist/types/src/storage/browser.js +15 -4
- package/dist/types/src/storage/browser.js.map +1 -1
- package/dist/types/src/storage/node.d.ts.map +1 -1
- package/dist/types/src/storage/node.js +1 -1
- package/dist/types/src/storage/node.js.map +1 -1
- package/dist/types/src/storage/node.test.js +1 -1
- package/dist/types/src/storage/node.test.js.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +36 -32
- package/src/cli-observability-secrets.json +2 -2
- package/src/extensions/otel/extension.ts +118 -17
- package/src/extensions/otel/logs.ts +6 -3
- package/src/extensions/otel/metrics.ts +2 -2
- package/src/extensions/otel/otel.ts +13 -0
- package/src/extensions/otel/span-processors.ts +45 -0
- package/src/extensions/otel/traces-browser.ts +95 -13
- package/src/extensions/otel/traces.ts +76 -12
- package/src/extensions/posthog/extension.ts +54 -27
- package/src/extensions/posthog/log-processor.test.ts +7 -6
- package/src/extensions/posthog/log-processor.ts +10 -21
- package/src/index.ts +0 -1
- package/src/observability-extension.ts +1 -1
- package/src/observability.test.ts +1 -1
- package/src/observability.ts +4 -2
- package/src/providers/client-observability.ts +2 -0
- package/src/providers/ip-data.ts +5 -2
- package/src/storage/browser.ts +14 -4
- package/src/storage/node.test.ts +1 -2
- package/src/storage/node.ts +1 -2
- package/src/vite-import-meta.d.ts +14 -0
- package/dist/lib/browser/chunk-K4VFBKST.mjs.map +0 -7
- package/dist/lib/browser/log-processor-FDLTDQEM.mjs.map +0 -7
- package/dist/lib/browser/logs-ATTRIUTL.mjs.map +0 -7
- package/dist/lib/browser/metrics-PKTV6IGF.mjs.map +0 -7
- package/dist/lib/browser/traces-browser-XYXBF5ZX.mjs +0 -62
- package/dist/lib/browser/traces-browser-XYXBF5ZX.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-FEVP3MK4.mjs.map +0 -7
- package/dist/lib/node-esm/log-processor-TKJVJJSJ.mjs.map +0 -7
- package/dist/lib/node-esm/logs-7J45KLM7.mjs.map +0 -7
- package/dist/lib/node-esm/metrics-H7DDLYSG.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/dist/types/src/log-buffer.d.ts +0 -34
- package/dist/types/src/log-buffer.d.ts.map +0 -1
- package/dist/types/src/log-buffer.js +0 -70
- package/dist/types/src/log-buffer.js.map +0 -1
- package/dist/types/src/log-buffer.test.d.ts +0 -2
- package/dist/types/src/log-buffer.test.d.ts.map +0 -1
- package/dist/types/src/log-buffer.test.js +0 -107
- package/dist/types/src/log-buffer.test.js.map +0 -1
- package/src/log-buffer.test.ts +0 -134
- package/src/log-buffer.ts +0 -101
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.f466a3d56e",
|
|
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",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/dxos/dxos"
|
|
10
10
|
},
|
|
11
|
-
"license": "
|
|
11
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
12
12
|
"author": "DXOS.org",
|
|
13
13
|
"sideEffects": false,
|
|
14
14
|
"type": "module",
|
|
@@ -29,48 +29,52 @@
|
|
|
29
29
|
"src"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@opentelemetry/api": "^1.9.
|
|
33
|
-
"@opentelemetry/api-logs": "^0.
|
|
34
|
-
"@opentelemetry/auto-instrumentations-web": "^0.
|
|
35
|
-
"@opentelemetry/
|
|
36
|
-
"@opentelemetry/exporter-
|
|
37
|
-
"@opentelemetry/exporter-
|
|
38
|
-
"@opentelemetry/
|
|
39
|
-
"@opentelemetry/
|
|
40
|
-
"@opentelemetry/
|
|
41
|
-
"@opentelemetry/sdk-
|
|
42
|
-
"@opentelemetry/sdk-
|
|
43
|
-
"@opentelemetry/sdk-trace-
|
|
44
|
-
"@opentelemetry/
|
|
32
|
+
"@opentelemetry/api": "^1.9.1",
|
|
33
|
+
"@opentelemetry/api-logs": "^0.218.0",
|
|
34
|
+
"@opentelemetry/auto-instrumentations-web": "^0.63.0",
|
|
35
|
+
"@opentelemetry/core": "^2.7.1",
|
|
36
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.218.0",
|
|
37
|
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.218.0",
|
|
38
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.218.0",
|
|
39
|
+
"@opentelemetry/instrumentation": "^0.218.0",
|
|
40
|
+
"@opentelemetry/resources": "^2.7.1",
|
|
41
|
+
"@opentelemetry/sdk-logs": "^0.218.0",
|
|
42
|
+
"@opentelemetry/sdk-metrics": "^2.7.1",
|
|
43
|
+
"@opentelemetry/sdk-trace-base": "^2.7.1",
|
|
44
|
+
"@opentelemetry/sdk-trace-web": "^2.7.1",
|
|
45
|
+
"@opentelemetry/semantic-conventions": "^1.41.1",
|
|
45
46
|
"debug": "^4.3.4",
|
|
46
47
|
"js-yaml": "4.1.1",
|
|
47
48
|
"localforage": "^1.10.0",
|
|
48
|
-
"posthog-js": "^1.
|
|
49
|
+
"posthog-js": "^1.353.0",
|
|
49
50
|
"uuid": "^9.0.0",
|
|
50
|
-
"
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/client-
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/
|
|
55
|
-
"@dxos/client-
|
|
56
|
-
"@dxos/
|
|
57
|
-
"@dxos/
|
|
58
|
-
"@dxos/log": "0.8.4-main.
|
|
59
|
-
"@dxos/
|
|
60
|
-
"@dxos/
|
|
61
|
-
"@dxos/
|
|
62
|
-
"@dxos/tracing": "0.8.4-main.
|
|
63
|
-
"@dxos/util": "0.8.4-main.
|
|
51
|
+
"zone.js": "^0.15.0",
|
|
52
|
+
"@dxos/async": "0.8.4-main.f466a3d56e",
|
|
53
|
+
"@dxos/client-services": "0.8.4-main.f466a3d56e",
|
|
54
|
+
"@dxos/client": "0.8.4-main.f466a3d56e",
|
|
55
|
+
"@dxos/invariant": "0.8.4-main.f466a3d56e",
|
|
56
|
+
"@dxos/client-protocol": "0.8.4-main.f466a3d56e",
|
|
57
|
+
"@dxos/config": "0.8.4-main.f466a3d56e",
|
|
58
|
+
"@dxos/debug": "0.8.4-main.f466a3d56e",
|
|
59
|
+
"@dxos/log": "0.8.4-main.f466a3d56e",
|
|
60
|
+
"@dxos/node-std": "0.8.4-main.f466a3d56e",
|
|
61
|
+
"@dxos/log-store-idb": "0.8.4-main.f466a3d56e",
|
|
62
|
+
"@dxos/network-manager": "0.8.4-main.f466a3d56e",
|
|
63
|
+
"@dxos/tracing": "0.8.4-main.f466a3d56e",
|
|
64
|
+
"@dxos/util": "0.8.4-main.f466a3d56e",
|
|
65
|
+
"@dxos/protocols": "0.8.4-main.f466a3d56e",
|
|
66
|
+
"@dxos/context": "0.8.4-main.f466a3d56e"
|
|
64
67
|
},
|
|
65
68
|
"devDependencies": {
|
|
66
69
|
"@effect/platform": "0.94.4",
|
|
67
70
|
"@types/debug": "^4.1.10",
|
|
68
71
|
"@types/js-yaml": "^4.0.5",
|
|
69
|
-
"
|
|
72
|
+
"@dxos/effect": "0.8.4-main.f466a3d56e",
|
|
73
|
+
"@dxos/messaging": "0.8.4-main.f466a3d56e"
|
|
70
74
|
},
|
|
71
75
|
"peerDependencies": {
|
|
72
76
|
"@effect/platform": "0.94.4",
|
|
73
|
-
"effect": "3.
|
|
77
|
+
"effect": "3.20.0"
|
|
74
78
|
},
|
|
75
79
|
"publishConfig": {
|
|
76
80
|
"access": "public"
|
|
@@ -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,12 +57,17 @@ 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
|
-
const
|
|
64
|
+
const rawEndpoint = isNode()
|
|
64
65
|
? (process.env.DX_OTEL_ENDPOINT ?? _endpoint ?? buildSecrets.OTEL_ENDPOINT)
|
|
65
|
-
: config.values.runtime?.app?.env?.DX_OTEL_ENDPOINT;
|
|
66
|
+
: (config.values.runtime?.app?.env?.DX_OTEL_ENDPOINT ?? _endpoint);
|
|
67
|
+
// The OTLP exporter (>= 0.203) validates URLs and rejects relative paths.
|
|
68
|
+
// In the browser/worker, resolve relative endpoints against the current origin
|
|
69
|
+
// so callers can keep using paths like `/api/otel` for proxied deployments.
|
|
70
|
+
const endpoint = !isNode() && rawEndpoint?.startsWith('/') ? resolveRelativeEndpoint(rawEndpoint) : rawEndpoint;
|
|
66
71
|
const headers =
|
|
67
72
|
_headers ??
|
|
68
73
|
Match.value(isNode()).pipe(
|
|
@@ -73,23 +78,49 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
|
|
|
73
78
|
Option.getOrElse(() => undefined),
|
|
74
79
|
);
|
|
75
80
|
|
|
76
|
-
if (!endpoint
|
|
77
|
-
log.info('Missing OTEL_ENDPOINT
|
|
81
|
+
if (!endpoint) {
|
|
82
|
+
log.info('Missing OTEL_ENDPOINT');
|
|
78
83
|
return stubExtension;
|
|
79
84
|
}
|
|
85
|
+
// Headers are optional when using a proxy that injects auth server-side.
|
|
86
|
+
const resolvedHeaders = headers ?? {};
|
|
87
|
+
// OTLP HTTP exporters require an absolute URL. Resolve relative paths using the current origin.
|
|
88
|
+
// globalThis.location is defined in all browser contexts (main thread, dedicated/service workers).
|
|
89
|
+
const resolvedEndpoint =
|
|
90
|
+
!isNode() && endpoint.startsWith('/') ? `${globalThis.location.origin}${endpoint}` : endpoint;
|
|
91
|
+
|
|
92
|
+
// Matches edge's `ctx.tag` span attribute (stamped by the edge log middleware
|
|
93
|
+
// when it reads the `X-DXOS-Client-Tag` header, see
|
|
94
|
+
// /edge/packages/services/edge/src/log-middleware.ts).
|
|
95
|
+
//
|
|
96
|
+
// Stamped in TWO places:
|
|
97
|
+
// 1. Resource attribute — so logs and metrics emitted from this extension
|
|
98
|
+
// also carry it, and for zero per-span cost.
|
|
99
|
+
// 2. Span attribute via `tags` — because edge puts `ctx.tag` in the span-
|
|
100
|
+
// attribute context (not resource), and SigNoz indexes the two contexts
|
|
101
|
+
// separately. Stamping it on span attributes here keeps a single
|
|
102
|
+
// `ctx.tag = <value>` filter matching spans from both tiers in SigNoz
|
|
103
|
+
// without requiring qualified `attribute.ctx.tag`/`resource.ctx.tag` syntax.
|
|
104
|
+
const clientTag = resolveTelemetryTag(config);
|
|
105
|
+
if (clientTag) {
|
|
106
|
+
tags.set('ctx.tag', clientTag);
|
|
107
|
+
}
|
|
80
108
|
|
|
81
109
|
const resource = defaultResource().merge(
|
|
82
110
|
resourceFromAttributes({
|
|
83
111
|
[ATTR_SERVICE_NAME]: serviceName,
|
|
84
112
|
[ATTR_SERVICE_VERSION]: serviceVersion,
|
|
113
|
+
'session.id': crypto.randomUUID(),
|
|
85
114
|
'deployment.environment': environment,
|
|
115
|
+
'dxos.process.type': detectProcessType(),
|
|
116
|
+
...(clientTag ? { 'ctx.tag': clientTag } : {}),
|
|
86
117
|
}),
|
|
87
118
|
);
|
|
88
119
|
|
|
89
120
|
const logs = logsEnabled
|
|
90
121
|
? new OtelLogs({
|
|
91
|
-
endpoint,
|
|
92
|
-
headers,
|
|
122
|
+
endpoint: resolvedEndpoint,
|
|
123
|
+
headers: resolvedHeaders,
|
|
93
124
|
resource,
|
|
94
125
|
getTags: () => Object.fromEntries(tags),
|
|
95
126
|
logLevel,
|
|
@@ -98,8 +129,8 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
|
|
|
98
129
|
|
|
99
130
|
const metrics = metricsEnabled
|
|
100
131
|
? new OtelMetrics({
|
|
101
|
-
endpoint,
|
|
102
|
-
headers,
|
|
132
|
+
endpoint: resolvedEndpoint,
|
|
133
|
+
headers: resolvedHeaders,
|
|
103
134
|
resource,
|
|
104
135
|
getTags: () => Object.fromEntries(tags),
|
|
105
136
|
})
|
|
@@ -107,16 +138,20 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
|
|
|
107
138
|
|
|
108
139
|
const traces = tracesEnabled
|
|
109
140
|
? new OtelTraces({
|
|
110
|
-
endpoint,
|
|
111
|
-
headers,
|
|
141
|
+
endpoint: resolvedEndpoint,
|
|
142
|
+
headers: resolvedHeaders,
|
|
112
143
|
resource,
|
|
113
144
|
getTags: () => Object.fromEntries(tags),
|
|
114
145
|
})
|
|
115
146
|
: undefined;
|
|
116
147
|
|
|
117
|
-
|
|
148
|
+
const extension: Extension = {
|
|
118
149
|
initialize: () =>
|
|
119
150
|
Effect.sync(() => {
|
|
151
|
+
if (disabled) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
120
155
|
if (logs) {
|
|
121
156
|
log.runtimeConfig.processors.push(logs.logProcessor);
|
|
122
157
|
}
|
|
@@ -134,13 +169,31 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
|
|
|
134
169
|
}),
|
|
135
170
|
close: () =>
|
|
136
171
|
Effect.promise(async () => {
|
|
137
|
-
|
|
138
|
-
|
|
172
|
+
// Run logs/metrics close concurrently and swallow their failures so the
|
|
173
|
+
// tracer provider shutdown below ALWAYS runs. Without this, a rejection
|
|
174
|
+
// from logs or metrics would drop the tracer provider's BatchSpanProcessor
|
|
175
|
+
// queue on process exit, manifesting as "Missing Span" in SigNoz for any
|
|
176
|
+
// already-exported children.
|
|
177
|
+
const results = await Promise.allSettled([logs?.close(), metrics?.close()]);
|
|
178
|
+
for (const result of results) {
|
|
179
|
+
if (result.status === 'rejected') {
|
|
180
|
+
log.catch(result.reason);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Critical: shut down the tracer provider so BatchSpanProcessor drains its
|
|
184
|
+
// queue. Otherwise spans enqueued in the last 5s (close/teardown spans)
|
|
185
|
+
// are dropped on process exit.
|
|
186
|
+
await traces?.close();
|
|
139
187
|
}),
|
|
140
188
|
flush: () =>
|
|
141
189
|
Effect.promise(async () => {
|
|
142
|
-
await logs?.flush();
|
|
143
|
-
|
|
190
|
+
const results = await Promise.allSettled([logs?.flush(), metrics?.flush()]);
|
|
191
|
+
for (const result of results) {
|
|
192
|
+
if (result.status === 'rejected') {
|
|
193
|
+
log.catch(result.reason);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
await traces?.flush();
|
|
144
197
|
}),
|
|
145
198
|
setTags: (incomingTags) => {
|
|
146
199
|
for (const [key, value] of Object.entries(incomingTags)) {
|
|
@@ -164,8 +217,56 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
|
|
|
164
217
|
traces ? ({ kind: 'traces', isAvailable: () => Effect.succeed(true) } satisfies ExtensionApi) : undefined,
|
|
165
218
|
].filter(isNonNullable),
|
|
166
219
|
};
|
|
220
|
+
|
|
221
|
+
return extension;
|
|
167
222
|
});
|
|
168
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Synchronous best-effort check for observability opt-out.
|
|
226
|
+
* Prevents telemetry from being emitted before the async storage check completes.
|
|
227
|
+
*/
|
|
228
|
+
const isObservabilityDisabledSync = (serviceName: string): boolean => {
|
|
229
|
+
if (isNode()) {
|
|
230
|
+
return process.env.DX_DISABLE_OBSERVABILITY === 'true';
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
if (typeof localStorage !== 'undefined') {
|
|
234
|
+
return localStorage.getItem(`${serviceName}/observability-disabled`) === 'true';
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// localStorage not available (e.g., in workers).
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/** Best-effort detection of the JavaScript execution context type. */
|
|
243
|
+
const detectProcessType = (): string => {
|
|
244
|
+
if (isNode()) {
|
|
245
|
+
return 'node';
|
|
246
|
+
}
|
|
247
|
+
if (typeof window !== 'undefined') {
|
|
248
|
+
return 'browser';
|
|
249
|
+
}
|
|
250
|
+
if (typeof (globalThis as any).ServiceWorkerGlobalScope !== 'undefined') {
|
|
251
|
+
return 'service-worker';
|
|
252
|
+
}
|
|
253
|
+
if (typeof (globalThis as any).SharedWorkerGlobalScope !== 'undefined') {
|
|
254
|
+
return 'shared-worker';
|
|
255
|
+
}
|
|
256
|
+
return 'dedicated-worker';
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Resolves a relative OTLP endpoint path against the current global origin.
|
|
261
|
+
* Workers expose the same `globalThis.location` as the page that spawned them;
|
|
262
|
+
* if neither is available we fall back to the original path and let the OTLP
|
|
263
|
+
* exporter surface the configuration error.
|
|
264
|
+
*/
|
|
265
|
+
const resolveRelativeEndpoint = (path: string): string => {
|
|
266
|
+
const origin = (globalThis as { location?: { origin?: string } }).location?.origin;
|
|
267
|
+
return origin ? `${origin}${path}` : path;
|
|
268
|
+
};
|
|
269
|
+
|
|
169
270
|
const parseHeaders = (unparsedHeaders: string): Record<string, string> => {
|
|
170
271
|
return unparsedHeaders.split(';').reduce((acc: Record<string, string>, header) => {
|
|
171
272
|
const [key, ...rest] = header.split(':');
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
getRelativeFilename,
|
|
17
17
|
} from '@dxos/log';
|
|
18
18
|
|
|
19
|
-
import { type OtelOptions, setDiagLogger } from './otel';
|
|
19
|
+
import { type OtelOptions, resolveOtlpUrl, setDiagLogger } from './otel';
|
|
20
20
|
|
|
21
21
|
const FLATTEN_DEPTH = 1;
|
|
22
22
|
|
|
@@ -35,7 +35,7 @@ export class OtelLogs {
|
|
|
35
35
|
constructor(private readonly options: OtelLogOptions) {
|
|
36
36
|
setDiagLogger(options.consoleDiagLogLevel);
|
|
37
37
|
const logExporter = new OTLPLogExporter({
|
|
38
|
-
url: this.options.endpoint + '/v1/logs',
|
|
38
|
+
url: resolveOtlpUrl(this.options.endpoint + '/v1/logs'),
|
|
39
39
|
headers: this.options.headers,
|
|
40
40
|
concurrencyLimit: 10, // an optional limit on pending requests
|
|
41
41
|
});
|
|
@@ -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
|
|
|
@@ -9,7 +9,7 @@ import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk
|
|
|
9
9
|
import { log } from '@dxos/log';
|
|
10
10
|
import { type MetricData, TRACE_PROCESSOR } from '@dxos/tracing';
|
|
11
11
|
|
|
12
|
-
import { type OtelOptions, setDiagLogger } from './otel';
|
|
12
|
+
import { type OtelOptions, resolveOtlpUrl, setDiagLogger } from './otel';
|
|
13
13
|
|
|
14
14
|
const EXPORT_INTERVAL = 60 * 1000;
|
|
15
15
|
|
|
@@ -24,7 +24,7 @@ export class OtelMetrics {
|
|
|
24
24
|
|
|
25
25
|
const grafanaMetricReader = new PeriodicExportingMetricReader({
|
|
26
26
|
exporter: new OTLPMetricExporter({
|
|
27
|
-
url: this.options.endpoint + '/v1/metrics',
|
|
27
|
+
url: resolveOtlpUrl(this.options.endpoint + '/v1/metrics'),
|
|
28
28
|
headers: this.options.headers,
|
|
29
29
|
}),
|
|
30
30
|
exportIntervalMillis: EXPORT_INTERVAL,
|
|
@@ -19,3 +19,16 @@ export const setDiagLogger = (level?: string) => {
|
|
|
19
19
|
diag.setLogger(new DiagConsoleLogger(), logLevel);
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a possibly-relative OTLP exporter URL to an absolute URL.
|
|
25
|
+
* The OpenTelemetry browser HTTP exporter validates user-provided URLs with `new URL(url)`,
|
|
26
|
+
* which throws on relative paths like `/api/otel/v1/logs`. In a browser context we resolve
|
|
27
|
+
* against `window.location.origin`; outside the browser we return the input unchanged.
|
|
28
|
+
*/
|
|
29
|
+
export const resolveOtlpUrl = (url: string): string => {
|
|
30
|
+
if (typeof window !== 'undefined' && url.startsWith('/')) {
|
|
31
|
+
return new URL(url, window.location.origin).toString();
|
|
32
|
+
}
|
|
33
|
+
return url;
|
|
34
|
+
};
|
|
@@ -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,37 +2,61 @@
|
|
|
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';
|
|
14
|
+
import { W3CTraceContextPropagator } from '@opentelemetry/core';
|
|
7
15
|
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
8
16
|
import { registerInstrumentations } from '@opentelemetry/instrumentation';
|
|
9
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
AlwaysOnSampler,
|
|
19
|
+
BatchSpanProcessor,
|
|
20
|
+
ParentBasedSampler,
|
|
21
|
+
TraceIdRatioBasedSampler,
|
|
22
|
+
} from '@opentelemetry/sdk-trace-base';
|
|
10
23
|
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
|
|
11
24
|
import { ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
|
|
12
25
|
|
|
13
26
|
import { log } from '@dxos/log';
|
|
14
|
-
import { type StartSpanOptions, TRACE_PROCESSOR } from '@dxos/tracing';
|
|
27
|
+
import { TRACE_ALL_KEY, type RemoteSpan, type StartSpanOptions, TRACE_PROCESSOR } from '@dxos/tracing';
|
|
15
28
|
|
|
16
|
-
import { type OtelOptions } from './otel';
|
|
29
|
+
import { type OtelOptions, resolveOtlpUrl } from './otel';
|
|
30
|
+
import { TagInjectorSpanProcessor } from './span-processors';
|
|
17
31
|
|
|
18
32
|
export class OtelTraces {
|
|
19
33
|
private _tracer: Tracer;
|
|
34
|
+
private readonly _tracerProvider: WebTracerProvider;
|
|
20
35
|
|
|
21
36
|
constructor(private readonly options: OtelOptions) {
|
|
22
|
-
|
|
37
|
+
propagation.setGlobalPropagator(new W3CTraceContextPropagator());
|
|
38
|
+
|
|
39
|
+
const forceTraceAll = typeof localStorage !== 'undefined' && localStorage.getItem(TRACE_ALL_KEY) === 'true';
|
|
40
|
+
|
|
41
|
+
this._tracerProvider = new WebTracerProvider({
|
|
23
42
|
resource: this.options.resource,
|
|
43
|
+
sampler: new ParentBasedSampler({
|
|
44
|
+
root: forceTraceAll ? new AlwaysOnSampler() : new TraceIdRatioBasedSampler(0.3),
|
|
45
|
+
}),
|
|
24
46
|
spanProcessors: [
|
|
47
|
+
new TagInjectorSpanProcessor(this.options.getTags),
|
|
25
48
|
new BatchSpanProcessor(
|
|
26
49
|
new OTLPTraceExporter({
|
|
27
|
-
url: this.options.endpoint + '/v1/traces',
|
|
50
|
+
url: resolveOtlpUrl(this.options.endpoint + '/v1/traces'),
|
|
28
51
|
headers: this.options.headers,
|
|
29
|
-
concurrencyLimit: 10,
|
|
52
|
+
concurrencyLimit: 10,
|
|
30
53
|
}),
|
|
54
|
+
{ scheduledDelayMillis: 5_000 },
|
|
31
55
|
),
|
|
32
56
|
],
|
|
33
57
|
});
|
|
34
58
|
|
|
35
|
-
trace.setGlobalTracerProvider(
|
|
59
|
+
trace.setGlobalTracerProvider(this._tracerProvider);
|
|
36
60
|
|
|
37
61
|
this._tracer = trace.getTracer(
|
|
38
62
|
'dxos-observability',
|
|
@@ -40,18 +64,76 @@ export class OtelTraces {
|
|
|
40
64
|
);
|
|
41
65
|
}
|
|
42
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
|
+
|
|
43
89
|
public start(): void {
|
|
44
90
|
log('trace processor registered');
|
|
45
91
|
|
|
46
92
|
registerInstrumentations({
|
|
47
|
-
instrumentations: [
|
|
93
|
+
instrumentations: [
|
|
94
|
+
getWebAutoInstrumentations({
|
|
95
|
+
'@opentelemetry/instrumentation-fetch': { enabled: false, ignoreUrls: [/api\.ipdata\.co/] },
|
|
96
|
+
'@opentelemetry/instrumentation-document-load': { enabled: false },
|
|
97
|
+
'@opentelemetry/instrumentation-xml-http-request': { enabled: false },
|
|
98
|
+
'@opentelemetry/instrumentation-user-interaction': { enabled: false },
|
|
99
|
+
}),
|
|
100
|
+
],
|
|
48
101
|
});
|
|
49
102
|
|
|
50
|
-
|
|
51
|
-
|
|
103
|
+
const tracer = this._tracer;
|
|
104
|
+
|
|
105
|
+
TRACE_PROCESSOR.tracingBackend = {
|
|
106
|
+
startSpan: (options: StartSpanOptions): RemoteSpan => {
|
|
52
107
|
log('begin otel trace', { options });
|
|
53
|
-
|
|
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
|
+
|
|
126
|
+
return {
|
|
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,
|
|
135
|
+
};
|
|
54
136
|
},
|
|
55
|
-
}
|
|
137
|
+
};
|
|
56
138
|
}
|
|
57
139
|
}
|