@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.
Files changed (133) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +14 -11
  3. package/dist/lib/browser/{chunk-K4VFBKST.mjs → chunk-PSFTIG54.mjs} +9 -2
  4. package/dist/lib/browser/chunk-PSFTIG54.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +181 -283
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/{log-processor-FDLTDQEM.mjs → log-processor-HPHWNBOK.mjs} +11 -18
  8. package/dist/lib/browser/log-processor-HPHWNBOK.mjs.map +7 -0
  9. package/dist/lib/browser/{logs-ATTRIUTL.mjs → logs-UTNIFYHF.mjs} +7 -4
  10. package/dist/lib/browser/logs-UTNIFYHF.mjs.map +7 -0
  11. package/dist/lib/browser/meta.json +1 -1
  12. package/dist/lib/browser/{metrics-PKTV6IGF.mjs → metrics-PRGSYAZJ.mjs} +7 -21
  13. package/dist/lib/browser/metrics-PRGSYAZJ.mjs.map +7 -0
  14. package/dist/lib/browser/traces-browser-XRINKQUA.mjs +154 -0
  15. package/dist/lib/browser/traces-browser-XRINKQUA.mjs.map +7 -0
  16. package/dist/lib/node-esm/{chunk-FEVP3MK4.mjs → chunk-EDDZWPYV.mjs} +9 -2
  17. package/dist/lib/node-esm/chunk-EDDZWPYV.mjs.map +7 -0
  18. package/dist/lib/node-esm/index.mjs +166 -250
  19. package/dist/lib/node-esm/index.mjs.map +4 -4
  20. package/dist/lib/node-esm/{log-processor-TKJVJJSJ.mjs → log-processor-ZODHERST.mjs} +11 -18
  21. package/dist/lib/node-esm/log-processor-ZODHERST.mjs.map +7 -0
  22. package/dist/lib/node-esm/{logs-7J45KLM7.mjs → logs-ARJUVN3T.mjs} +7 -4
  23. package/dist/lib/node-esm/logs-ARJUVN3T.mjs.map +7 -0
  24. package/dist/lib/node-esm/meta.json +1 -1
  25. package/dist/lib/node-esm/{metrics-H7DDLYSG.mjs → metrics-DREJOOAC.mjs} +7 -21
  26. package/dist/lib/node-esm/metrics-DREJOOAC.mjs.map +7 -0
  27. package/dist/lib/node-esm/traces-74F7JUKF.mjs +125 -0
  28. package/dist/lib/node-esm/traces-74F7JUKF.mjs.map +7 -0
  29. package/dist/types/src/cli-observability-secrets.json +2 -2
  30. package/dist/types/src/extensions/otel/extension.d.ts.map +1 -1
  31. package/dist/types/src/extensions/otel/extension.js +112 -16
  32. package/dist/types/src/extensions/otel/extension.js.map +1 -1
  33. package/dist/types/src/extensions/otel/logs.d.ts.map +1 -1
  34. package/dist/types/src/extensions/otel/logs.js +6 -3
  35. package/dist/types/src/extensions/otel/logs.js.map +1 -1
  36. package/dist/types/src/extensions/otel/metrics.d.ts.map +1 -1
  37. package/dist/types/src/extensions/otel/metrics.js +2 -2
  38. package/dist/types/src/extensions/otel/metrics.js.map +1 -1
  39. package/dist/types/src/extensions/otel/otel.d.ts +7 -0
  40. package/dist/types/src/extensions/otel/otel.d.ts.map +1 -1
  41. package/dist/types/src/extensions/otel/otel.js +12 -0
  42. package/dist/types/src/extensions/otel/otel.js.map +1 -1
  43. package/dist/types/src/extensions/otel/span-processors.d.ts +25 -0
  44. package/dist/types/src/extensions/otel/span-processors.d.ts.map +1 -0
  45. package/dist/types/src/extensions/otel/span-processors.js +41 -0
  46. package/dist/types/src/extensions/otel/span-processors.js.map +1 -0
  47. package/dist/types/src/extensions/otel/traces-browser.d.ts +17 -0
  48. package/dist/types/src/extensions/otel/traces-browser.d.ts.map +1 -1
  49. package/dist/types/src/extensions/otel/traces-browser.js +73 -12
  50. package/dist/types/src/extensions/otel/traces-browser.js.map +1 -1
  51. package/dist/types/src/extensions/otel/traces.d.ts +17 -0
  52. package/dist/types/src/extensions/otel/traces.d.ts.map +1 -1
  53. package/dist/types/src/extensions/otel/traces.js +59 -9
  54. package/dist/types/src/extensions/otel/traces.js.map +1 -1
  55. package/dist/types/src/extensions/posthog/extension.d.ts +13 -0
  56. package/dist/types/src/extensions/posthog/extension.d.ts.map +1 -1
  57. package/dist/types/src/extensions/posthog/extension.js +41 -28
  58. package/dist/types/src/extensions/posthog/extension.js.map +1 -1
  59. package/dist/types/src/extensions/posthog/log-processor.d.ts.map +1 -1
  60. package/dist/types/src/extensions/posthog/log-processor.js +10 -19
  61. package/dist/types/src/extensions/posthog/log-processor.js.map +1 -1
  62. package/dist/types/src/extensions/posthog/log-processor.test.js +2 -2
  63. package/dist/types/src/extensions/posthog/log-processor.test.js.map +1 -1
  64. package/dist/types/src/index.d.ts +0 -1
  65. package/dist/types/src/index.d.ts.map +1 -1
  66. package/dist/types/src/index.js +0 -1
  67. package/dist/types/src/index.js.map +1 -1
  68. package/dist/types/src/observability-extension.d.ts +1 -1
  69. package/dist/types/src/observability-extension.d.ts.map +1 -1
  70. package/dist/types/src/observability.d.ts.map +1 -1
  71. package/dist/types/src/observability.js +4 -2
  72. package/dist/types/src/observability.js.map +1 -1
  73. package/dist/types/src/observability.test.js +1 -1
  74. package/dist/types/src/observability.test.js.map +1 -1
  75. package/dist/types/src/providers/client-observability.d.ts.map +1 -1
  76. package/dist/types/src/providers/client-observability.js +2 -0
  77. package/dist/types/src/providers/client-observability.js.map +1 -1
  78. package/dist/types/src/providers/ip-data.d.ts.map +1 -1
  79. package/dist/types/src/providers/ip-data.js +4 -2
  80. package/dist/types/src/providers/ip-data.js.map +1 -1
  81. package/dist/types/src/storage/browser.d.ts.map +1 -1
  82. package/dist/types/src/storage/browser.js +15 -4
  83. package/dist/types/src/storage/browser.js.map +1 -1
  84. package/dist/types/src/storage/node.d.ts.map +1 -1
  85. package/dist/types/src/storage/node.js +1 -1
  86. package/dist/types/src/storage/node.js.map +1 -1
  87. package/dist/types/src/storage/node.test.js +1 -1
  88. package/dist/types/src/storage/node.test.js.map +1 -1
  89. package/dist/types/tsconfig.tsbuildinfo +1 -1
  90. package/package.json +36 -32
  91. package/src/cli-observability-secrets.json +2 -2
  92. package/src/extensions/otel/extension.ts +118 -17
  93. package/src/extensions/otel/logs.ts +6 -3
  94. package/src/extensions/otel/metrics.ts +2 -2
  95. package/src/extensions/otel/otel.ts +13 -0
  96. package/src/extensions/otel/span-processors.ts +45 -0
  97. package/src/extensions/otel/traces-browser.ts +95 -13
  98. package/src/extensions/otel/traces.ts +76 -12
  99. package/src/extensions/posthog/extension.ts +54 -27
  100. package/src/extensions/posthog/log-processor.test.ts +7 -6
  101. package/src/extensions/posthog/log-processor.ts +10 -21
  102. package/src/index.ts +0 -1
  103. package/src/observability-extension.ts +1 -1
  104. package/src/observability.test.ts +1 -1
  105. package/src/observability.ts +4 -2
  106. package/src/providers/client-observability.ts +2 -0
  107. package/src/providers/ip-data.ts +5 -2
  108. package/src/storage/browser.ts +14 -4
  109. package/src/storage/node.test.ts +1 -2
  110. package/src/storage/node.ts +1 -2
  111. package/src/vite-import-meta.d.ts +14 -0
  112. package/dist/lib/browser/chunk-K4VFBKST.mjs.map +0 -7
  113. package/dist/lib/browser/log-processor-FDLTDQEM.mjs.map +0 -7
  114. package/dist/lib/browser/logs-ATTRIUTL.mjs.map +0 -7
  115. package/dist/lib/browser/metrics-PKTV6IGF.mjs.map +0 -7
  116. package/dist/lib/browser/traces-browser-XYXBF5ZX.mjs +0 -62
  117. package/dist/lib/browser/traces-browser-XYXBF5ZX.mjs.map +0 -7
  118. package/dist/lib/node-esm/chunk-FEVP3MK4.mjs.map +0 -7
  119. package/dist/lib/node-esm/log-processor-TKJVJJSJ.mjs.map +0 -7
  120. package/dist/lib/node-esm/logs-7J45KLM7.mjs.map +0 -7
  121. package/dist/lib/node-esm/metrics-H7DDLYSG.mjs.map +0 -7
  122. package/dist/lib/node-esm/traces-KMTHMYFX.mjs +0 -44
  123. package/dist/lib/node-esm/traces-KMTHMYFX.mjs.map +0 -7
  124. package/dist/types/src/log-buffer.d.ts +0 -34
  125. package/dist/types/src/log-buffer.d.ts.map +0 -1
  126. package/dist/types/src/log-buffer.js +0 -70
  127. package/dist/types/src/log-buffer.js.map +0 -1
  128. package/dist/types/src/log-buffer.test.d.ts +0 -2
  129. package/dist/types/src/log-buffer.test.d.ts.map +0 -1
  130. package/dist/types/src/log-buffer.test.js +0 -107
  131. package/dist/types/src/log-buffer.test.js.map +0 -1
  132. package/src/log-buffer.test.ts +0 -134
  133. 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.ef1bc66f44",
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": "MIT",
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.0",
33
- "@opentelemetry/api-logs": "^0.203.0",
34
- "@opentelemetry/auto-instrumentations-web": "^0.51.0",
35
- "@opentelemetry/exporter-logs-otlp-http": "^0.203.0",
36
- "@opentelemetry/exporter-metrics-otlp-http": "^0.203.0",
37
- "@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
38
- "@opentelemetry/instrumentation": "^0.203.0",
39
- "@opentelemetry/resources": "^2.1.0",
40
- "@opentelemetry/sdk-logs": "^0.203.0",
41
- "@opentelemetry/sdk-metrics": "^2.1.0",
42
- "@opentelemetry/sdk-trace-base": "^2.1.0",
43
- "@opentelemetry/sdk-trace-web": "^2.1.0",
44
- "@opentelemetry/semantic-conventions": "^1.37.0",
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.347.2",
49
+ "posthog-js": "^1.353.0",
49
50
  "uuid": "^9.0.0",
50
- "@dxos/async": "0.8.4-main.ef1bc66f44",
51
- "@dxos/client": "0.8.4-main.ef1bc66f44",
52
- "@dxos/client-protocol": "0.8.4-main.ef1bc66f44",
53
- "@dxos/config": "0.8.4-main.ef1bc66f44",
54
- "@dxos/context": "0.8.4-main.ef1bc66f44",
55
- "@dxos/client-services": "0.8.4-main.ef1bc66f44",
56
- "@dxos/debug": "0.8.4-main.ef1bc66f44",
57
- "@dxos/invariant": "0.8.4-main.ef1bc66f44",
58
- "@dxos/log": "0.8.4-main.ef1bc66f44",
59
- "@dxos/network-manager": "0.8.4-main.ef1bc66f44",
60
- "@dxos/protocols": "0.8.4-main.ef1bc66f44",
61
- "@dxos/node-std": "0.8.4-main.ef1bc66f44",
62
- "@dxos/tracing": "0.8.4-main.ef1bc66f44",
63
- "@dxos/util": "0.8.4-main.ef1bc66f44"
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
- "zone.js": "^0.15.0"
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.19.16"
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": "https://ingest.eu.signoz.cloud:443",
5
- "OTEL_HEADERS": "signoz-ingestion-key: 5300e0c9-a5e8-4a10-9c61-f473e539c0b7"
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 enabledRef = yield* Ref.make(!cachedDisabled);
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 endpoint = isNode()
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 || !headers) {
77
- log.info('Missing OTEL_ENDPOINT or OTEL_HEADERS');
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
- return {
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
- await logs?.close();
138
- await metrics?.close();
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
- await metrics?.flush();
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.ERROR;
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 { type Tracer, trace } from '@opentelemetry/api';
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 { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
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
- const tracerProvider = new WebTracerProvider({
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, // an optional limit on pending requests
52
+ concurrencyLimit: 10,
30
53
  }),
54
+ { scheduledDelayMillis: 5_000 },
31
55
  ),
32
56
  ],
33
57
  });
34
58
 
35
- trace.setGlobalTracerProvider(tracerProvider);
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: [getWebAutoInstrumentations()],
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
- TRACE_PROCESSOR.remoteTracing.registerProcessor({
51
- startSpan: (options: StartSpanOptions) => {
103
+ const tracer = this._tracer;
104
+
105
+ TRACE_PROCESSOR.tracingBackend = {
106
+ startSpan: (options: StartSpanOptions): RemoteSpan => {
52
107
  log('begin otel trace', { options });
53
- return this._tracer.startSpan(options.name, options);
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
  }