@dxos/observability 0.14.0 → 0.16.0

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 (62) hide show
  1. package/dist/lib/Observability.mjs +1 -248
  2. package/dist/lib/ai/AiObservability.mjs +1 -238
  3. package/dist/lib/chunk-AiObservability.mjs +238 -0
  4. package/dist/lib/chunk-AiObservability.mjs.map +1 -0
  5. package/dist/lib/chunk-Observability.mjs +248 -0
  6. package/dist/lib/chunk-Observability.mjs.map +1 -0
  7. package/dist/lib/chunk-ObservabilityClientProvider.mjs.map +1 -1
  8. package/dist/lib/chunk-ObservabilityExtension.mjs +38 -11
  9. package/dist/lib/chunk-ObservabilityExtension.mjs.map +1 -1
  10. package/dist/lib/chunk-ObservabilityProvider.mjs.map +1 -1
  11. package/dist/lib/chunk-OtelLogSink.mjs +105 -0
  12. package/dist/lib/{extensions/otel/OtelLogSink.mjs.map → chunk-OtelLogSink.mjs.map} +1 -1
  13. package/dist/lib/chunk-OtelMetricsSink.mjs +49 -0
  14. package/dist/lib/{extensions/otel/OtelMetricsSink.mjs.map → chunk-OtelMetricsSink.mjs.map} +1 -1
  15. package/dist/lib/chunk-OtelSpanSink.mjs.map +1 -1
  16. package/dist/lib/chunk-extensions.mjs.map +1 -1
  17. package/dist/lib/chunk-logs.mjs.map +1 -1
  18. package/dist/lib/chunk-support-logs.mjs +1 -1
  19. package/dist/lib/chunk-support-logs.mjs.map +1 -1
  20. package/dist/lib/extensions/otel/OtelLogSink.mjs +1 -105
  21. package/dist/lib/extensions/otel/OtelMetricsSink.mjs +1 -49
  22. package/dist/lib/extensions/otel/OtelSpanSink.mjs +1 -2
  23. package/dist/lib/index.mjs +4 -4
  24. package/dist/lib/posthog-transport.node.mjs.map +1 -1
  25. package/dist/lib/providers/ObservabilityClientProvider.mjs +1 -2
  26. package/dist/lib/storage.node.mjs.map +1 -1
  27. package/dist/lib/traces.mjs.map +1 -1
  28. package/dist/lib/traces.node.mjs.map +1 -1
  29. package/dist/types/src/ObservabilityExtension.d.ts +1 -1
  30. package/dist/types/src/ObservabilityExtension.d.ts.map +1 -1
  31. package/dist/types/src/ai/AiObservability.js +2 -2
  32. package/dist/types/src/ai/AiObservability.js.map +1 -1
  33. package/dist/types/src/ai/AiObservability.test.js +11 -11
  34. package/dist/types/src/ai/AiObservability.test.js.map +1 -1
  35. package/dist/types/src/extensions/otel/tail-sampling.test.js +1 -1
  36. package/dist/types/src/extensions/otel/tail-sampling.test.js.map +1 -1
  37. package/dist/types/src/extensions/posthog/before-send.d.ts +4 -0
  38. package/dist/types/src/extensions/posthog/before-send.d.ts.map +1 -0
  39. package/dist/types/src/extensions/posthog/before-send.js +19 -0
  40. package/dist/types/src/extensions/posthog/before-send.js.map +1 -0
  41. package/dist/types/src/extensions/posthog/before-send.test.d.ts +2 -0
  42. package/dist/types/src/extensions/posthog/before-send.test.d.ts.map +1 -0
  43. package/dist/types/src/extensions/posthog/before-send.test.js +30 -0
  44. package/dist/types/src/extensions/posthog/before-send.test.js.map +1 -0
  45. package/dist/types/src/extensions/posthog/extension.d.ts +10 -0
  46. package/dist/types/src/extensions/posthog/extension.d.ts.map +1 -1
  47. package/dist/types/src/extensions/posthog/extension.js +22 -2
  48. package/dist/types/src/extensions/posthog/extension.js.map +1 -1
  49. package/dist/types/src/version.d.ts +1 -1
  50. package/dist/types/src/version.js +1 -1
  51. package/dist/types/tsconfig.tsbuildinfo +1 -1
  52. package/package.json +20 -20
  53. package/src/ObservabilityExtension.ts +1 -1
  54. package/src/ai/AiObservability.test.ts +12 -11
  55. package/src/ai/AiObservability.ts +2 -2
  56. package/src/extensions/otel/tail-sampling.test.ts +1 -1
  57. package/src/extensions/posthog/before-send.test.ts +36 -0
  58. package/src/extensions/posthog/before-send.ts +26 -0
  59. package/src/extensions/posthog/extension.ts +30 -1
  60. package/src/version.ts +1 -1
  61. package/dist/lib/Observability.mjs.map +0 -1
  62. package/dist/lib/ai/AiObservability.mjs.map +0 -1
@@ -0,0 +1,105 @@
1
+ import { t as __exportAll } from "./chunk-rolldown-runtime.mjs";
2
+ import { n as contextForTrace } from "./chunk-trace-context.mjs";
3
+ import { n as convertLevel, t as OtelLogs } from "./chunk-logs.mjs";
4
+ import { LogLevel, log, shortLevelName } from "@dxos/log";
5
+ import { defaultResource, resourceFromAttributes } from "@opentelemetry/resources";
6
+ //#region src/extensions/otel/OtelLogSink.ts
7
+ var OtelLogSink_exports = /* @__PURE__ */ __exportAll({ Sink: () => Sink });
8
+ var __dxlog_file = "/__w/dxos/dxos/packages/sdk/observability/src/extensions/otel/OtelLogSink.ts";
9
+ var levelFromShortName = new Map([
10
+ LogLevel.TRACE,
11
+ LogLevel.DEBUG,
12
+ LogLevel.VERBOSE,
13
+ LogLevel.INFO,
14
+ LogLevel.WARN,
15
+ LogLevel.ERROR
16
+ ].map((level) => [shortLevelName[level], level]));
17
+ var Sink = class {
18
+ #logs;
19
+ #tags;
20
+ #logLevel;
21
+ #onTraceFlagged;
22
+ constructor(init, options = {}) {
23
+ this.#tags = { ...init.tags };
24
+ this.#logLevel = init.logLevel;
25
+ this.#onTraceFlagged = options.onTraceFlagged;
26
+ this.#logs = new OtelLogs({
27
+ destinations: init.destinations,
28
+ resource: createResource(init.resourceAttributes),
29
+ getTags: () => this.#tags,
30
+ logLevel: init.logLevel,
31
+ exporter: options.exporter,
32
+ batch: options.batch
33
+ });
34
+ }
35
+ /**
36
+ * Ingest one pre-serialized JSONL log line (the same string the IDB store appends).
37
+ * Never throws — a malformed line is dropped.
38
+ */
39
+ append(line) {
40
+ let record;
41
+ try {
42
+ record = JSON.parse(line);
43
+ } catch {
44
+ return;
45
+ }
46
+ if (record === null || typeof record !== "object") return;
47
+ const level = levelFromShortName.get(record.l);
48
+ if (level === void 0) return;
49
+ if (level >= LogLevel.WARN && record.r !== void 0) this.#onTraceFlagged?.(record.r);
50
+ if (level < this.#logLevel) return;
51
+ this.#logs.emit({
52
+ severityNumber: convertLevel(level),
53
+ body: record.m,
54
+ timestamp: new Date(record.t),
55
+ attributes: {
56
+ ...record.f !== void 0 ? { meta: {
57
+ file: record.f,
58
+ line: record.n
59
+ } } : {},
60
+ ...record.e !== void 0 ? { error: record.e } : {},
61
+ ...parseContext(record.c)
62
+ },
63
+ context: record.r !== void 0 && record.s !== void 0 ? contextForTrace({
64
+ traceId: record.r,
65
+ spanId: record.s
66
+ }) : void 0
67
+ });
68
+ }
69
+ handleMessage(message) {
70
+ switch (message.type) {
71
+ case "otel-tags":
72
+ this.#tags = {
73
+ ...this.#tags,
74
+ ...message.tags
75
+ };
76
+ break;
77
+ case "otel-flush": this.flush().catch((err) => log.catch(err, void 0, {
78
+ "~LogMeta": "~LogMeta",
79
+ F: __dxlog_file,
80
+ L: 115,
81
+ S: this
82
+ }));
83
+ }
84
+ }
85
+ flush() {
86
+ return this.#logs.flush();
87
+ }
88
+ close() {
89
+ return this.#logs.close();
90
+ }
91
+ };
92
+ var createResource = (attributes) => defaultResource().merge(resourceFromAttributes(attributes));
93
+ var parseContext = (context) => {
94
+ if (!context) return {};
95
+ try {
96
+ const parsed = JSON.parse(context);
97
+ return Object.fromEntries(Object.entries(parsed).filter(([, value]) => value !== void 0).map(([key, value]) => [`ctx_${key}`, typeof value === "string" ? value : JSON.stringify(value)]));
98
+ } catch {
99
+ return {};
100
+ }
101
+ };
102
+ //#endregion
103
+ export { Sink as n, OtelLogSink_exports as t };
104
+
105
+ //# sourceMappingURL=chunk-OtelLogSink.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"OtelLogSink.mjs","names":[],"sources":["../../../../src/extensions/otel/OtelLogSink.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\n// The log-writer worker imports this through its own subpath, so nothing else loads the\n// OpenTelemetry SDK.\n\nimport { type Resource, defaultResource, resourceFromAttributes } from '@opentelemetry/resources';\nimport { type LogRecordExporter } from '@opentelemetry/sdk-logs';\n\nimport { type LogRecord as JsonlLogRecord, LogLevel, log, shortLevelName } from '@dxos/log';\n\nimport { OtelLogs, convertLevel } from './logs.ts';\nimport { type OtelDestination } from './otel.ts';\nimport { contextForTrace } from './trace-context.ts';\n\nexport type Init = {\n type: 'otel-init';\n destinations: OtelDestination[];\n resourceAttributes: Record<string, string>;\n logLevel: LogLevel;\n tags: Record<string, string>;\n};\n\nexport type Message = Init | { type: 'otel-tags'; tags: Record<string, string> } | { type: 'otel-flush' };\n\nconst levelFromShortName = new Map<string, LogLevel>(\n [LogLevel.TRACE, LogLevel.DEBUG, LogLevel.VERBOSE, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR].map((level) => [\n shortLevelName[level],\n level,\n ]),\n);\n\nexport type Options = {\n exporter?: LogRecordExporter;\n batch?: { maxQueueSize?: number; maxExportBatchSize?: number };\n /**\n * Called with the trace id of every record at warning or above that names one, before the export\n * level is applied: a warning the sink does not export still marks its trace as worth keeping.\n */\n onTraceFlagged?: (traceId: string) => void;\n};\n\nexport class Sink {\n readonly #logs: OtelLogs;\n #tags: Record<string, string>;\n readonly #logLevel: LogLevel;\n readonly #onTraceFlagged?: (traceId: string) => void;\n\n constructor(init: Init, options: Options = {}) {\n this.#tags = { ...init.tags };\n this.#logLevel = init.logLevel;\n this.#onTraceFlagged = options.onTraceFlagged;\n this.#logs = new OtelLogs({\n destinations: init.destinations,\n resource: createResource(init.resourceAttributes),\n getTags: () => this.#tags,\n logLevel: init.logLevel,\n exporter: options.exporter,\n batch: options.batch,\n });\n }\n\n /**\n * Ingest one pre-serialized JSONL log line (the same string the IDB store appends).\n * Never throws — a malformed line is dropped.\n */\n append(line: string): void {\n let record: JsonlLogRecord | null;\n try {\n record = JSON.parse(line);\n } catch {\n return;\n }\n // `JSON.parse` also succeeds on bare primitives; only an object carries the record fields.\n if (record === null || typeof record !== 'object') {\n return;\n }\n const level = levelFromShortName.get(record.l);\n if (level === undefined) {\n return;\n }\n if (level >= LogLevel.WARN && record.r !== undefined) {\n this.#onTraceFlagged?.(record.r);\n }\n if (level < this.#logLevel) {\n return;\n }\n\n this.#logs.emit({\n severityNumber: convertLevel(level),\n body: record.m,\n timestamp: new Date(record.t),\n attributes: {\n ...(record.f !== undefined ? { meta: { file: record.f, line: record.n } } : {}),\n ...(record.e !== undefined ? { error: record.e } : {}),\n ...parseContext(record.c),\n },\n context:\n record.r !== undefined && record.s !== undefined\n ? contextForTrace({ traceId: record.r, spanId: record.s })\n : undefined,\n });\n }\n\n handleMessage(message: Exclude<Message, Init>): void {\n switch (message.type) {\n case 'otel-tags': {\n this.#tags = { ...this.#tags, ...message.tags };\n break;\n }\n case 'otel-flush': {\n void this.flush().catch((err) => log.catch(err));\n break;\n }\n }\n }\n\n flush(): Promise<void> {\n return this.#logs.flush();\n }\n\n close(): Promise<void> {\n return this.#logs.close();\n }\n}\n\nconst createResource = (attributes: Record<string, string>): Resource =>\n defaultResource().merge(resourceFromAttributes(attributes));\n\nconst parseContext = (context: string | undefined): Record<string, string> => {\n if (!context) {\n return {};\n }\n try {\n const parsed: Record<string, unknown> = JSON.parse(context);\n return Object.fromEntries(\n Object.entries(parsed)\n .filter(([, value]) => value !== undefined)\n .map(([key, value]) => [`ctx_${key}`, typeof value === 'string' ? value : JSON.stringify(value)]),\n );\n } catch {\n return {};\n }\n};\n"],"mappings":";;;;;;;;AA4BA,IAAM,qBAAqB,IAAI,IAC7B;CAAC,SAAS;CAAO,SAAS;CAAO,SAAS;CAAS,SAAS;CAAM,SAAS;CAAM,SAAS;AAAK,CAAC,CAAC,KAAK,UAAU,CAC9G,eAAe,QACf,KACF,CAAC,CACH;AAYA,IAAa,OAAb,MAAkB;CAChB;CACA;CACA;CACA;CAEA,YAAY,MAAY,UAAmB,CAAC,GAAG;EAC7C,KAAK,QAAQ,EAAE,GAAG,KAAK,KAAK;EAC5B,KAAK,YAAY,KAAK;EACtB,KAAK,kBAAkB,QAAQ;EAC/B,KAAK,QAAQ,IAAI,SAAS;GACxB,cAAc,KAAK;GACnB,UAAU,eAAe,KAAK,kBAAkB;GAChD,eAAe,KAAK;GACpB,UAAU,KAAK;GACf,UAAU,QAAQ;GAClB,OAAO,QAAQ;EACjB,CAAC;CACH;;;;;CAMA,OAAO,MAAoB;EACzB,IAAI;EACJ,IAAI;GACF,SAAS,KAAK,MAAM,IAAI;EAC1B,QAAQ;GACN;EACF;EAEA,IAAI,WAAW,QAAQ,OAAO,WAAW,UACvC;EAEF,MAAM,QAAQ,mBAAmB,IAAI,OAAO,CAAC;EAC7C,IAAI,UAAU,KAAA,GACZ;EAEF,IAAI,SAAS,SAAS,QAAQ,OAAO,MAAM,KAAA,GACzC,KAAK,kBAAkB,OAAO,CAAC;EAEjC,IAAI,QAAQ,KAAK,WACf;EAGF,KAAK,MAAM,KAAK;GACd,gBAAgB,aAAa,KAAK;GAClC,MAAM,OAAO;GACb,WAAW,IAAI,KAAK,OAAO,CAAC;GAC5B,YAAY;IACV,GAAI,OAAO,MAAM,KAAA,IAAY,EAAE,MAAM;KAAE,MAAM,OAAO;KAAG,MAAM,OAAO;IAAE,EAAE,IAAI,CAAC;IAC7E,GAAI,OAAO,MAAM,KAAA,IAAY,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC;IACpD,GAAG,aAAa,OAAO,CAAC;GAC1B;GACA,SACE,OAAO,MAAM,KAAA,KAAa,OAAO,MAAM,KAAA,IACnC,gBAAgB;IAAE,SAAS,OAAO;IAAG,QAAQ,OAAO;GAAE,CAAC,IACvD,KAAA;EACR,CAAC;CACH;CAEA,cAAc,SAAuC;EACnD,QAAQ,QAAQ,MAAhB;GACE,KAAK;IACH,KAAK,QAAQ;KAAE,GAAG,KAAK;KAAO,GAAG,QAAQ;IAAK;IAC9C;GAEF,KAAK,cACH,KAAU,MAAM,CAAC,CAAC,OAAO,QAAQ,IAAI,MAAM,KAAE,KAAA,GAAA;IAAA,YAAA;IAAA,GAAA;IAAA,GAAA;IAAA,GAAA;GAAA,CAAC,CAAC;EAGnD;CACF;CAEA,QAAuB;EACrB,OAAO,KAAK,MAAM,MAAM;CAC1B;CAEA,QAAuB;EACrB,OAAO,KAAK,MAAM,MAAM;CAC1B;AACF;AAEA,IAAM,kBAAkB,eACtB,gBAAgB,CAAC,CAAC,MAAM,uBAAuB,UAAU,CAAC;AAE5D,IAAM,gBAAgB,YAAwD;CAC5E,IAAI,CAAC,SACH,OAAO,CAAC;CAEV,IAAI;EACF,MAAM,SAAkC,KAAK,MAAM,OAAO;EAC1D,OAAO,OAAO,YACZ,OAAO,QAAQ,MAAM,CAAA,CAClB,QAAQ,GAAG,WAAW,UAAU,KAAA,CAAS,CAAA,CACzC,KAAK,CAAC,KAAK,WAAW,CAAC,OAAO,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK,CAAC,CAAC,CACpG;CACF,QAAQ;EACN,OAAO,CAAC;CACV;AACF"}
1
+ {"version":3,"file":"chunk-OtelLogSink.mjs","names":[],"sources":["../../src/extensions/otel/OtelLogSink.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\n// The log-writer worker imports this through its own subpath, so nothing else loads the\n// OpenTelemetry SDK.\n\nimport { type Resource, defaultResource, resourceFromAttributes } from '@opentelemetry/resources';\nimport { type LogRecordExporter } from '@opentelemetry/sdk-logs';\n\nimport { type LogRecord as JsonlLogRecord, LogLevel, log, shortLevelName } from '@dxos/log';\n\nimport { OtelLogs, convertLevel } from './logs.ts';\nimport { type OtelDestination } from './otel.ts';\nimport { contextForTrace } from './trace-context.ts';\n\nexport type Init = {\n type: 'otel-init';\n destinations: OtelDestination[];\n resourceAttributes: Record<string, string>;\n logLevel: LogLevel;\n tags: Record<string, string>;\n};\n\nexport type Message = Init | { type: 'otel-tags'; tags: Record<string, string> } | { type: 'otel-flush' };\n\nconst levelFromShortName = new Map<string, LogLevel>(\n [LogLevel.TRACE, LogLevel.DEBUG, LogLevel.VERBOSE, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR].map((level) => [\n shortLevelName[level],\n level,\n ]),\n);\n\nexport type Options = {\n exporter?: LogRecordExporter;\n batch?: { maxQueueSize?: number; maxExportBatchSize?: number };\n /**\n * Called with the trace id of every record at warning or above that names one, before the export\n * level is applied: a warning the sink does not export still marks its trace as worth keeping.\n */\n onTraceFlagged?: (traceId: string) => void;\n};\n\nexport class Sink {\n readonly #logs: OtelLogs;\n #tags: Record<string, string>;\n readonly #logLevel: LogLevel;\n readonly #onTraceFlagged?: (traceId: string) => void;\n\n constructor(init: Init, options: Options = {}) {\n this.#tags = { ...init.tags };\n this.#logLevel = init.logLevel;\n this.#onTraceFlagged = options.onTraceFlagged;\n this.#logs = new OtelLogs({\n destinations: init.destinations,\n resource: createResource(init.resourceAttributes),\n getTags: () => this.#tags,\n logLevel: init.logLevel,\n exporter: options.exporter,\n batch: options.batch,\n });\n }\n\n /**\n * Ingest one pre-serialized JSONL log line (the same string the IDB store appends).\n * Never throws — a malformed line is dropped.\n */\n append(line: string): void {\n let record: JsonlLogRecord | null;\n try {\n record = JSON.parse(line);\n } catch {\n return;\n }\n // `JSON.parse` also succeeds on bare primitives; only an object carries the record fields.\n if (record === null || typeof record !== 'object') {\n return;\n }\n const level = levelFromShortName.get(record.l);\n if (level === undefined) {\n return;\n }\n if (level >= LogLevel.WARN && record.r !== undefined) {\n this.#onTraceFlagged?.(record.r);\n }\n if (level < this.#logLevel) {\n return;\n }\n\n this.#logs.emit({\n severityNumber: convertLevel(level),\n body: record.m,\n timestamp: new Date(record.t),\n attributes: {\n ...(record.f !== undefined ? { meta: { file: record.f, line: record.n } } : {}),\n ...(record.e !== undefined ? { error: record.e } : {}),\n ...parseContext(record.c),\n },\n context:\n record.r !== undefined && record.s !== undefined\n ? contextForTrace({ traceId: record.r, spanId: record.s })\n : undefined,\n });\n }\n\n handleMessage(message: Exclude<Message, Init>): void {\n switch (message.type) {\n case 'otel-tags': {\n this.#tags = { ...this.#tags, ...message.tags };\n break;\n }\n case 'otel-flush': {\n void this.flush().catch((err) => log.catch(err));\n break;\n }\n }\n }\n\n flush(): Promise<void> {\n return this.#logs.flush();\n }\n\n close(): Promise<void> {\n return this.#logs.close();\n }\n}\n\nconst createResource = (attributes: Record<string, string>): Resource =>\n defaultResource().merge(resourceFromAttributes(attributes));\n\nconst parseContext = (context: string | undefined): Record<string, string> => {\n if (!context) {\n return {};\n }\n try {\n const parsed: Record<string, unknown> = JSON.parse(context);\n return Object.fromEntries(\n Object.entries(parsed)\n .filter(([, value]) => value !== undefined)\n .map(([key, value]) => [`ctx_${key}`, typeof value === 'string' ? value : JSON.stringify(value)]),\n );\n } catch {\n return {};\n }\n};\n"],"mappings":";;;;;;;;AA4BA,IAAM,qBAAqB,IAAI,IAC7B;CAAC,SAAS;CAAO,SAAS;CAAO,SAAS;CAAS,SAAS;CAAM,SAAS;CAAM,SAAS;AAAK,CAAC,CAAC,KAAK,UAAU,CAC9G,eAAe,QACf,KACF,CAAC,CACH;AAYA,IAAa,OAAb,MAAkB;CAChB;CACA;CACA;CACA;CAEA,YAAY,MAAY,UAAmB,CAAC,GAAG;EAC7C,KAAK,QAAQ,EAAE,GAAG,KAAK,KAAK;EAC5B,KAAK,YAAY,KAAK;EACtB,KAAK,kBAAkB,QAAQ;EAC/B,KAAK,QAAQ,IAAI,SAAS;GACxB,cAAc,KAAK;GACnB,UAAU,eAAe,KAAK,kBAAkB;GAChD,eAAe,KAAK;GACpB,UAAU,KAAK;GACf,UAAU,QAAQ;GAClB,OAAO,QAAQ;EACjB,CAAC;CACH;;;;;CAMA,OAAO,MAAoB;EACzB,IAAI;EACJ,IAAI;GACF,SAAS,KAAK,MAAM,IAAI;EAC1B,QAAQ;GACN;EACF;EAEA,IAAI,WAAW,QAAQ,OAAO,WAAW,UACvC;EAEF,MAAM,QAAQ,mBAAmB,IAAI,OAAO,CAAC;EAC7C,IAAI,UAAU,KAAA,GACZ;EAEF,IAAI,SAAS,SAAS,QAAQ,OAAO,MAAM,KAAA,GACzC,KAAK,kBAAkB,OAAO,CAAC;EAEjC,IAAI,QAAQ,KAAK,WACf;EAGF,KAAK,MAAM,KAAK;GACd,gBAAgB,aAAa,KAAK;GAClC,MAAM,OAAO;GACb,WAAW,IAAI,KAAK,OAAO,CAAC;GAC5B,YAAY;IACV,GAAI,OAAO,MAAM,KAAA,IAAY,EAAE,MAAM;KAAE,MAAM,OAAO;KAAG,MAAM,OAAO;IAAE,EAAE,IAAI,CAAC;IAC7E,GAAI,OAAO,MAAM,KAAA,IAAY,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC;IACpD,GAAG,aAAa,OAAO,CAAC;GAC1B;GACA,SACE,OAAO,MAAM,KAAA,KAAa,OAAO,MAAM,KAAA,IACnC,gBAAgB;IAAE,SAAS,OAAO;IAAG,QAAQ,OAAO;GAAE,CAAC,IACvD,KAAA;EACR,CAAC;CACH;CAEA,cAAc,SAAuC;EACnD,QAAQ,QAAQ,MAAhB;GACE,KAAK;IACH,KAAK,QAAQ;KAAE,GAAG,KAAK;KAAO,GAAG,QAAQ;IAAK;IAC9C;GAEF,KAAK,cACH,KAAU,MAAM,CAAC,CAAC,OAAO,QAAQ,IAAI,MAAM,KAAE,KAAA,GAAA;IAAA,YAAA;IAAA,GAAA;IAAA,GAAA;IAAA,GAAA;GAAA,CAAC,CAAC;EAGnD;CACF;CAEA,QAAuB;EACrB,OAAO,KAAK,MAAM,MAAM;CAC1B;CAEA,QAAuB;EACrB,OAAO,KAAK,MAAM,MAAM;CAC1B;AACF;AAEA,IAAM,kBAAkB,eACtB,gBAAgB,CAAC,CAAC,MAAM,uBAAuB,UAAU,CAAC;AAE5D,IAAM,gBAAgB,YAAwD;CAC5E,IAAI,CAAC,SACH,OAAO,CAAC;CAEV,IAAI;EACF,MAAM,SAAkC,KAAK,MAAM,OAAO;EAC1D,OAAO,OAAO,YACZ,OAAO,QAAQ,MAAM,CAAA,CAClB,QAAQ,GAAG,WAAW,UAAU,KAAA,CAAS,CAAA,CACzC,KAAK,CAAC,KAAK,WAAW,CAAC,OAAO,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK,CAAC,CAAC,CACpG;CACF,QAAQ;EACN,OAAO,CAAC;CACV;AACF"}
@@ -0,0 +1,49 @@
1
+ import { t as __exportAll } from "./chunk-rolldown-runtime.mjs";
2
+ import { t as OtelMetrics } from "./chunk-metrics.mjs";
3
+ import { defaultResource, resourceFromAttributes } from "@opentelemetry/resources";
4
+ //#region src/extensions/otel/OtelMetricsSink.ts
5
+ var OtelMetricsSink_exports = /* @__PURE__ */ __exportAll({ Sink: () => Sink });
6
+ var Sink = class {
7
+ #metrics;
8
+ #tags;
9
+ constructor(init, options = {}) {
10
+ this.#tags = { ...init.tags };
11
+ this.#metrics = new OtelMetrics({
12
+ destinations: init.destinations,
13
+ resource: defaultResource().merge(resourceFromAttributes(init.resourceAttributes)),
14
+ getTags: () => this.#tags,
15
+ exporter: options.exporter,
16
+ registerTraceProcessor: false
17
+ });
18
+ }
19
+ append(batch) {
20
+ for (const metric of batch.metrics) for (const value of metric.values) this.#record(metric, value);
21
+ }
22
+ setTags(tags) {
23
+ this.#tags = {
24
+ ...this.#tags,
25
+ ...tags
26
+ };
27
+ }
28
+ flush() {
29
+ return this.#metrics.flush();
30
+ }
31
+ close() {
32
+ return this.#metrics.close();
33
+ }
34
+ #record(metric, value) {
35
+ switch (metric.op) {
36
+ case "increment":
37
+ this.#metrics.increment(metric.name, value, metric.tags, metric.meta);
38
+ break;
39
+ case "distribution":
40
+ this.#metrics.distribution(metric.name, value, metric.tags, metric.meta);
41
+ break;
42
+ case "gauge": this.#metrics.gauge(metric.name, value, metric.tags, metric.meta);
43
+ }
44
+ }
45
+ };
46
+ //#endregion
47
+ export { Sink as n, OtelMetricsSink_exports as t };
48
+
49
+ //# sourceMappingURL=chunk-OtelMetricsSink.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"OtelMetricsSink.mjs","names":[],"sources":["../../../../src/extensions/otel/OtelMetricsSink.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\n// The log-writer worker imports this through its own subpath, so nothing else loads the\n// OpenTelemetry SDK.\n\nimport { type Attributes } from '@opentelemetry/api';\nimport { defaultResource, resourceFromAttributes } from '@opentelemetry/resources';\nimport { type PushMetricExporter } from '@opentelemetry/sdk-metrics';\n\nimport { OtelMetrics } from './metrics.ts';\nimport { type OtelDestination } from './otel.ts';\n\nexport type Init = {\n type: 'otel-metrics-init';\n destinations: OtelDestination[];\n resourceAttributes: Record<string, string>;\n tags: Record<string, string>;\n};\n\n/**\n * One instrument series aggregated on the producer side: an increment carries its sum, a gauge its\n * latest value, a distribution every recorded value.\n */\nexport type Metric = {\n op: 'increment' | 'distribution' | 'gauge';\n name: string;\n values: number[];\n tags?: Attributes;\n meta?: { unit?: string; description?: string };\n};\n\n/** Everything a producer recorded since its previous batch, in one message. */\nexport type Batch = {\n type: 'otel-metric-batch';\n metrics: Metric[];\n};\n\nexport type Message = Init | Batch;\n\nexport type Options = {\n exporter?: PushMetricExporter;\n};\n\nexport class Sink {\n readonly #metrics: OtelMetrics;\n #tags: Record<string, string>;\n\n constructor(init: Init, options: Options = {}) {\n this.#tags = { ...init.tags };\n this.#metrics = new OtelMetrics({\n destinations: init.destinations,\n resource: defaultResource().merge(resourceFromAttributes(init.resourceAttributes)),\n getTags: () => this.#tags,\n exporter: options.exporter,\n registerTraceProcessor: false,\n });\n }\n\n append(batch: Batch): void {\n for (const metric of batch.metrics) {\n for (const value of metric.values) {\n this.#record(metric, value);\n }\n }\n }\n\n setTags(tags: Record<string, string>): void {\n this.#tags = { ...this.#tags, ...tags };\n }\n\n flush(): Promise<void> {\n return this.#metrics.flush();\n }\n\n close(): Promise<void> {\n return this.#metrics.close();\n }\n\n #record(metric: Metric, value: number): void {\n switch (metric.op) {\n case 'increment': {\n this.#metrics.increment(metric.name, value, metric.tags, metric.meta);\n break;\n }\n case 'distribution': {\n this.#metrics.distribution(metric.name, value, metric.tags, metric.meta);\n break;\n }\n case 'gauge': {\n this.#metrics.gauge(metric.name, value, metric.tags, metric.meta);\n break;\n }\n }\n }\n}\n"],"mappings":";;;;;AA+CA,IAAa,OAAb,MAAkB;CAChB;CACA;CAEA,YAAY,MAAY,UAAmB,CAAC,GAAG;EAC7C,KAAK,QAAQ,EAAE,GAAG,KAAK,KAAK;EAC5B,KAAK,WAAW,IAAI,YAAY;GAC9B,cAAc,KAAK;GACnB,UAAU,gBAAgB,CAAC,CAAC,MAAM,uBAAuB,KAAK,kBAAkB,CAAC;GACjF,eAAe,KAAK;GACpB,UAAU,QAAQ;GAClB,wBAAwB;EAC1B,CAAC;CACH;CAEA,OAAO,OAAoB;EACzB,KAAK,MAAM,UAAU,MAAM,SACzB,KAAK,MAAM,SAAS,OAAO,QACzB,KAAK,QAAQ,QAAQ,KAAK;CAGhC;CAEA,QAAQ,MAAoC;EAC1C,KAAK,QAAQ;GAAE,GAAG,KAAK;GAAO,GAAG;EAAK;CACxC;CAEA,QAAuB;EACrB,OAAO,KAAK,SAAS,MAAM;CAC7B;CAEA,QAAuB;EACrB,OAAO,KAAK,SAAS,MAAM;CAC7B;CAEA,QAAQ,QAAgB,OAAqB;EAC3C,QAAQ,OAAO,IAAf;GACE,KAAK;IACH,KAAK,SAAS,UAAU,OAAO,MAAM,OAAO,OAAO,MAAM,OAAO,IAAI;IACpE;GAEF,KAAK;IACH,KAAK,SAAS,aAAa,OAAO,MAAM,OAAO,OAAO,MAAM,OAAO,IAAI;IACvE;GAEF,KAAK,SACH,KAAK,SAAS,MAAM,OAAO,MAAM,OAAO,OAAO,MAAM,OAAO,IAAI;EAGpE;CACF;AACF"}
1
+ {"version":3,"file":"chunk-OtelMetricsSink.mjs","names":[],"sources":["../../src/extensions/otel/OtelMetricsSink.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\n// The log-writer worker imports this through its own subpath, so nothing else loads the\n// OpenTelemetry SDK.\n\nimport { type Attributes } from '@opentelemetry/api';\nimport { defaultResource, resourceFromAttributes } from '@opentelemetry/resources';\nimport { type PushMetricExporter } from '@opentelemetry/sdk-metrics';\n\nimport { OtelMetrics } from './metrics.ts';\nimport { type OtelDestination } from './otel.ts';\n\nexport type Init = {\n type: 'otel-metrics-init';\n destinations: OtelDestination[];\n resourceAttributes: Record<string, string>;\n tags: Record<string, string>;\n};\n\n/**\n * One instrument series aggregated on the producer side: an increment carries its sum, a gauge its\n * latest value, a distribution every recorded value.\n */\nexport type Metric = {\n op: 'increment' | 'distribution' | 'gauge';\n name: string;\n values: number[];\n tags?: Attributes;\n meta?: { unit?: string; description?: string };\n};\n\n/** Everything a producer recorded since its previous batch, in one message. */\nexport type Batch = {\n type: 'otel-metric-batch';\n metrics: Metric[];\n};\n\nexport type Message = Init | Batch;\n\nexport type Options = {\n exporter?: PushMetricExporter;\n};\n\nexport class Sink {\n readonly #metrics: OtelMetrics;\n #tags: Record<string, string>;\n\n constructor(init: Init, options: Options = {}) {\n this.#tags = { ...init.tags };\n this.#metrics = new OtelMetrics({\n destinations: init.destinations,\n resource: defaultResource().merge(resourceFromAttributes(init.resourceAttributes)),\n getTags: () => this.#tags,\n exporter: options.exporter,\n registerTraceProcessor: false,\n });\n }\n\n append(batch: Batch): void {\n for (const metric of batch.metrics) {\n for (const value of metric.values) {\n this.#record(metric, value);\n }\n }\n }\n\n setTags(tags: Record<string, string>): void {\n this.#tags = { ...this.#tags, ...tags };\n }\n\n flush(): Promise<void> {\n return this.#metrics.flush();\n }\n\n close(): Promise<void> {\n return this.#metrics.close();\n }\n\n #record(metric: Metric, value: number): void {\n switch (metric.op) {\n case 'increment': {\n this.#metrics.increment(metric.name, value, metric.tags, metric.meta);\n break;\n }\n case 'distribution': {\n this.#metrics.distribution(metric.name, value, metric.tags, metric.meta);\n break;\n }\n case 'gauge': {\n this.#metrics.gauge(metric.name, value, metric.tags, metric.meta);\n break;\n }\n }\n }\n}\n"],"mappings":";;;;;AA+CA,IAAa,OAAb,MAAkB;CAChB;CACA;CAEA,YAAY,MAAY,UAAmB,CAAC,GAAG;EAC7C,KAAK,QAAQ,EAAE,GAAG,KAAK,KAAK;EAC5B,KAAK,WAAW,IAAI,YAAY;GAC9B,cAAc,KAAK;GACnB,UAAU,gBAAgB,CAAC,CAAC,MAAM,uBAAuB,KAAK,kBAAkB,CAAC;GACjF,eAAe,KAAK;GACpB,UAAU,QAAQ;GAClB,wBAAwB;EAC1B,CAAC;CACH;CAEA,OAAO,OAAoB;EACzB,KAAK,MAAM,UAAU,MAAM,SACzB,KAAK,MAAM,SAAS,OAAO,QACzB,KAAK,QAAQ,QAAQ,KAAK;CAGhC;CAEA,QAAQ,MAAoC;EAC1C,KAAK,QAAQ;GAAE,GAAG,KAAK;GAAO,GAAG;EAAK;CACxC;CAEA,QAAuB;EACrB,OAAO,KAAK,SAAS,MAAM;CAC7B;CAEA,QAAuB;EACrB,OAAO,KAAK,SAAS,MAAM;CAC7B;CAEA,QAAQ,QAAgB,OAAqB;EAC3C,QAAQ,OAAO,IAAf;GACE,KAAK;IACH,KAAK,SAAS,UAAU,OAAO,MAAM,OAAO,OAAO,MAAM,OAAO,IAAI;IACpE;GAEF,KAAK;IACH,KAAK,SAAS,aAAa,OAAO,MAAM,OAAO,OAAO,MAAM,OAAO,IAAI;IACvE;GAEF,KAAK,SACH,KAAK,SAAS,MAAM,OAAO,MAAM,OAAO,OAAO,MAAM,OAAO,IAAI;EAGpE;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"chunk-OtelSpanSink.mjs","names":[],"sources":["../../src/extensions/otel/ai-content.ts","../../src/extensions/otel/tail-sampling.ts","../../src/extensions/otel/OtelSpanSink.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport { type Attributes, type Context } from '@opentelemetry/api';\nimport { type ReadableSpan, type Span, type SpanProcessor } from '@opentelemetry/sdk-trace-base';\n\nimport * as SpanAttributes from '@dxos/effect/SpanAttributes';\n\n/**\n * The prompt, the response, and the tool names. They stay on the span for the AI analytics sink,\n * which applies the content policy, and never leave the process with it: a trace backend gets the\n * model call's metadata.\n */\nexport const AI_CONTENT_ATTRIBUTES: ReadonlySet<string> = new Set([\n SpanAttributes.AI.input,\n SpanAttributes.AI.output,\n SpanAttributes.AI.tools,\n]);\n\nexport const withoutAiContent = (attributes: Attributes): Attributes =>\n Object.fromEntries(Object.entries(attributes).filter(([key]) => !AI_CONTENT_ATTRIBUTES.has(key)));\n\n/** A view of the span without its content attributes; the span itself is what other processors read. */\nexport const stripAiContent = (span: ReadableSpan): ReadableSpan =>\n Object.create(span, { attributes: { value: withoutAiContent(span.attributes), enumerable: true } });\n\n/** Strips AI content from every span it forwards. Sits directly in front of an exporter. */\nexport class AiContentStrippingSpanProcessor implements SpanProcessor {\n constructor(private readonly _delegate: SpanProcessor) {}\n\n onStart(span: Span, parentContext: Context): void {\n this._delegate.onStart(span, parentContext);\n }\n\n onEnd(span: ReadableSpan): void {\n this._delegate.onEnd(stripAiContent(span));\n }\n\n forceFlush(): Promise<void> {\n return this._delegate.forceFlush();\n }\n\n shutdown(): Promise<void> {\n return this._delegate.shutdown();\n }\n}\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type Attributes, type Context, SpanStatusCode } from '@opentelemetry/api';\nimport type { ReadableSpan, Span, SpanProcessor } from '@opentelemetry/sdk-trace-base';\n\nimport * as SpanAttributes from '@dxos/effect/SpanAttributes';\n\n/**\n * Tail sampling for spans arriving at the observability worker.\n *\n * Head sampling cannot express either rule we want. A sampler runs when a span is *created*, so it\n * cannot know that the span will fail, and it sees the span name before the provider has annotated\n * it. Both decisions are only available once the span has ended — which is what \"tail\" means. The\n * canonical implementation of this is the OpenTelemetry Collector's tail-sampling processor, which\n * buffers whole traces server-side; there is no collector between Composer and SigNoz, so the same\n * decisions are made here instead, on the spans this realm produced.\n *\n * The rules are the canonical tail-sampling set — keep what errored, keep what was slow, sample the\n * rest — as described in OpenTelemetry's own writeup and implemented by the Collector's\n * `tailsamplingprocessor` policies (`status_code`, `latency`, `probabilistic`):\n *\n * - https://opentelemetry.io/blog/2022/tail-sampling/\n * - https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/tailsamplingprocessor\n *\n * Expressing them as a processor that filters in `onEnd` is the documented way to drop spans from a\n * JS SDK, since a `Sampler` cannot: https://github.com/open-telemetry/opentelemetry-js/discussions/2817\n *\n * In order:\n * - a span that **errored** is kept, and its trace is promoted;\n * - a span slower than {@link DEFAULT_SLOW_MS} is kept, and its trace is promoted;\n * - a span marked `dxos.sampling.keep` — a rare flow such as an invitation, which a dashboard counts\n * one by one and a 30% sample would mostly hide — is kept, and its trace is promoted;\n * - a span carrying `gen_ai.*` or `dxos.ai.kind` — a model call, or the turn and tool calls\n * around it that the AI analytics sink reports — is kept, and its trace is promoted, so a model call is never a\n * fraction of the calls that happened. This one is ours rather than canonical: the AI events are\n * what price the product, and a sampled fraction of them reports a fraction of the spend;\n * - any other span of a **promoted** trace is kept;\n * - everything else is kept at {@link DEFAULT_RATIO}, keyed on the trace id so the decision is the\n * same for every span of a trace.\n *\n * Promotion is remembered rather than buffered: holding span payloads until a trace settles would\n * mean an unbounded buffer and a decision delay, and a conversation's trace can stay open for\n * minutes. Remembering only the trace id costs nothing and recovers the case that matters, since a\n * parent ends *after* its children — so promoting on the first error keeps the ancestors that\n * explain it. Siblings that already ended and were dropped are not recovered; that is the price of\n * not buffering, and it is why an errored trace can arrive with gaps rather than whole.\n */\n\n/** Kept fraction for a trace that nothing promoted. Matches the head ratio this replaced. */\nexport const DEFAULT_RATIO = 0.3;\n\n/**\n * Above this, a span is kept as slow.\n *\n * Far above the 5s the Collector's examples use, because this app's slowest spans are model calls\n * and a conversation turn legitimately runs for tens of seconds — a 5s threshold would promote\n * nearly every AI trace and quietly turn the ratio off for exactly the traffic there is most of.\n * At 30s what is left is a hang rather than a slow answer.\n */\nexport const DEFAULT_SLOW_MS = 30_000;\n\n/**\n * Bound on remembered trace ids. Reached only under a burst far above normal span rates, and a trace\n * evicted early degrades to the ratio rather than to a dropped span.\n */\nconst DEFAULT_MAX_TRACKED_TRACES = 10_000;\n\nexport type Options = {\n ratio?: number;\n slowMs?: number;\n maxTrackedTraces?: number;\n};\n\n/** The parts of an ended span the rules read, whether it arrived over a port or ended in-process. */\nexport type Decidable = {\n readonly traceId: string;\n readonly status: { readonly code: SpanStatusCode };\n readonly attributes: Attributes;\n readonly durationMs: number;\n};\n\nexport class TailSampler {\n private readonly _ratio: number;\n private readonly _slowMs: number;\n private readonly _maxTrackedTraces: number;\n /** Insertion-ordered, so the oldest entry is the first key — an LRU without the bookkeeping. */\n private readonly _promoted = new Set<string>();\n\n constructor({\n ratio = DEFAULT_RATIO,\n slowMs = DEFAULT_SLOW_MS,\n maxTrackedTraces = DEFAULT_MAX_TRACKED_TRACES,\n }: Options = {}) {\n this._ratio = ratio;\n this._slowMs = slowMs;\n this._maxTrackedTraces = maxTrackedTraces;\n }\n\n /** Whether the span should be forwarded to the exporter. */\n keep(span: Decidable): boolean {\n if (this._isPromotable(span)) {\n this.promote(span.traceId);\n return true;\n }\n return this._promoted.has(span.traceId) || sampledByTraceId(span.traceId, this._ratio);\n }\n\n private _isPromotable(span: Decidable): boolean {\n return (\n span.status.code === SpanStatusCode.ERROR ||\n span.durationMs > this._slowMs ||\n isKeepMarked(span.attributes) ||\n Object.keys(span.attributes).some(isAiAttribute)\n );\n }\n\n /**\n * Keeps every later span of the trace. Called for what the sampler cannot see itself: a log at\n * warning or above names a trace worth keeping the same way an errored span does.\n */\n promote(traceId: string): void {\n // Re-inserting moves the id to the end, so an active trace is not evicted ahead of a stale one.\n this._promoted.delete(traceId);\n this._promoted.add(traceId);\n while (this._promoted.size > this._maxTrackedTraces) {\n const oldest = this._promoted.values().next();\n if (oldest.done) {\n break;\n }\n this._promoted.delete(oldest.value);\n }\n }\n}\n\nconst GEN_AI_PREFIX = 'gen_ai.';\n\n/** `gen_ai.*` marks a model call; `dxos.ai.kind` the turn around it and the tool calls inside it. */\nconst isAiAttribute = (key: string): boolean => key.startsWith(GEN_AI_PREFIX) || key === SpanAttributes.AI.kind;\n\n/** Manual `@dxos/tracing` spans namespace their attributes under `ctx.`, so the marker arrives either way. */\nconst isKeepMarked = (attributes: Attributes): boolean =>\n attributes[SpanAttributes.SAMPLING.keep] === true || attributes[`ctx.${SpanAttributes.SAMPLING.keep}`] === true;\n\n/**\n * Deterministic per trace, so every span of a trace decides the same way. Reads the low 8 hex digits\n * of the 32-hex-digit trace id — the id is random, so any window is uniform, and 32 bits is well\n * inside the exact range of a double.\n */\nconst sampledByTraceId = (traceId: string, ratio: number): boolean => {\n if (ratio >= 1) {\n return true;\n }\n if (ratio <= 0 || traceId.length < 8) {\n return false;\n }\n const value = Number.parseInt(traceId.slice(-8), 16);\n return Number.isNaN(value) ? false : value / 0x1_0000_0000 < ratio;\n};\n\n/** OTel reports durations as `[seconds, nanoseconds]`. */\nexport const hrTimeToMs = ([seconds, nanos]: [number, number]): number => seconds * 1_000 + nanos / 1e6;\n\n/**\n * Applies the same rules in-process, for the path that exports without a worker (node, and a browser\n * with no observability worker). Without it that path would export everything, since head sampling\n * no longer thins anything.\n */\nexport class TailSamplingSpanProcessor implements SpanProcessor {\n private readonly _sampler: TailSampler;\n\n constructor(\n private readonly _delegate: SpanProcessor,\n options?: Options,\n ) {\n this._sampler = new TailSampler(options);\n }\n\n onStart(span: Span, parentContext: Context): void {\n this._delegate.onStart(span, parentContext);\n }\n\n /** See {@link TailSampler.promote}. */\n promote(traceId: string): void {\n this._sampler.promote(traceId);\n }\n\n onEnd(span: ReadableSpan): void {\n const { traceId } = span.spanContext();\n const decidable = {\n traceId,\n status: span.status,\n attributes: span.attributes,\n durationMs: hrTimeToMs(span.duration),\n };\n if (this._sampler.keep(decidable)) {\n this._delegate.onEnd(span);\n }\n }\n\n forceFlush(): Promise<void> {\n return this._delegate.forceFlush();\n }\n\n shutdown(): Promise<void> {\n return this._delegate.shutdown();\n }\n}\n","//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\n// The log-writer worker imports this through its own subpath, so nothing else loads the\n// OpenTelemetry SDK.\n\nimport {\n type Attributes,\n type Context,\n type HrTime,\n type Link,\n type SpanContext,\n type SpanKind,\n type SpanStatus,\n} from '@opentelemetry/api';\nimport { TraceState, hrTimeDuration } from '@opentelemetry/core';\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';\nimport { type Resource, defaultResource, resourceFromAttributes } from '@opentelemetry/resources';\nimport {\n BatchSpanProcessor,\n type ReadableSpan,\n type Span as SdkSpan,\n type SpanExporter,\n type SpanProcessor,\n} from '@opentelemetry/sdk-trace-base';\n\nimport * as AiContent from './ai-content.ts';\nimport { type OtelDestination, signalUrl } from './otel.ts';\nimport * as TailSampling from './tail-sampling.ts';\n\nexport type Init = {\n type: 'otel-traces-init';\n destinations: OtelDestination[];\n resourceAttributes: Record<string, string>;\n};\n\nexport type Span = {\n type: 'otel-span';\n name: string;\n kind: SpanKind;\n traceId: string;\n spanId: string;\n traceFlags: number;\n traceState?: string;\n parentSpanContext?: { traceId: string; spanId: string; traceFlags: number };\n startTime: HrTime;\n endTime: HrTime;\n status: SpanStatus;\n attributes: Attributes;\n events: { name: string; time: HrTime; attributes?: Attributes; droppedAttributesCount?: number }[];\n links: { context: { traceId: string; spanId: string; traceFlags: number }; attributes?: Attributes }[];\n droppedAttributesCount: number;\n droppedEventsCount: number;\n droppedLinksCount: number;\n instrumentationScope: { name: string; version?: string };\n};\n\nexport type Message = Init | Span;\n\nexport type Handle = { post: (record: Span) => void };\n\nexport const serializeReadableSpan = (span: ReadableSpan): Span => {\n const spanContext = span.spanContext();\n return {\n type: 'otel-span',\n name: span.name,\n kind: span.kind,\n traceId: spanContext.traceId,\n spanId: spanContext.spanId,\n traceFlags: spanContext.traceFlags,\n traceState: spanContext.traceState?.serialize(),\n parentSpanContext: span.parentSpanContext\n ? {\n traceId: span.parentSpanContext.traceId,\n spanId: span.parentSpanContext.spanId,\n traceFlags: span.parentSpanContext.traceFlags,\n }\n : undefined,\n startTime: span.startTime,\n endTime: span.endTime,\n status: span.status,\n attributes: { ...span.attributes },\n events: span.events.map((event) => ({\n name: event.name,\n time: event.time,\n attributes: event.attributes ? { ...event.attributes } : undefined,\n droppedAttributesCount: event.droppedAttributesCount,\n })),\n links: span.links.map((link) => ({\n context: {\n traceId: link.context.traceId,\n spanId: link.context.spanId,\n traceFlags: link.context.traceFlags,\n },\n attributes: link.attributes ? { ...link.attributes } : undefined,\n })),\n droppedAttributesCount: span.droppedAttributesCount,\n droppedEventsCount: span.droppedEventsCount,\n droppedLinksCount: span.droppedLinksCount,\n instrumentationScope: {\n name: span.instrumentationScope.name,\n version: span.instrumentationScope.version,\n },\n };\n};\n\nexport class PortSpanProcessor implements SpanProcessor {\n constructor(private readonly _post: (record: Span) => void) {}\n\n onStart(_span: SdkSpan, _parentContext: Context): void {}\n\n onEnd(span: ReadableSpan): void {\n this._post(serializeReadableSpan(span));\n }\n\n async shutdown(): Promise<void> {}\n\n async forceFlush(): Promise<void> {}\n}\n\nexport type Options = {\n exporter?: SpanExporter;\n /** Overrides the default tail-sampling rules; pass `{ ratio: 1 }` to export everything. */\n sampling?: TailSampling.Options;\n};\n\nexport class Sink {\n readonly #resource: Resource;\n readonly #processors: BatchSpanProcessor[];\n readonly #sampler: TailSampling.TailSampler;\n\n constructor(init: Init, options: Options = {}) {\n this.#sampler = new TailSampling.TailSampler(options.sampling);\n this.#resource = defaultResource().merge(resourceFromAttributes(init.resourceAttributes));\n this.#processors = init.destinations.map(\n (destination) =>\n new BatchSpanProcessor(\n options.exporter ??\n new OTLPTraceExporter({\n url: signalUrl(destination, 'traces'),\n headers: destination.headers,\n concurrencyLimit: 10,\n }),\n ),\n );\n }\n\n /** Keeps the rest of a trace something outside the span stream vouched for, e.g. a warning log. */\n promote(traceId: string): void {\n this.#sampler.promote(traceId);\n }\n\n append(record: Span): void {\n if (\n !this.#sampler.keep({\n traceId: record.traceId,\n status: record.status,\n attributes: record.attributes,\n durationMs: TailSampling.hrTimeToMs(hrTimeDuration(record.startTime, record.endTime)),\n })\n ) {\n return;\n }\n const span = this.#materialize(record);\n for (const processor of this.#processors) {\n processor.onEnd(span);\n }\n }\n\n async flush(): Promise<void> {\n await Promise.all(this.#processors.map((processor) => processor.forceFlush()));\n }\n\n async close(): Promise<void> {\n await Promise.all(this.#processors.map((processor) => processor.shutdown()));\n }\n\n #materialize(record: Span): ReadableSpan {\n const spanContext: SpanContext = {\n traceId: record.traceId,\n spanId: record.spanId,\n traceFlags: record.traceFlags,\n traceState: record.traceState !== undefined ? new TraceState(record.traceState) : undefined,\n };\n const links: Link[] = record.links.map((link) => ({\n context: { ...link.context },\n attributes: link.attributes,\n }));\n return {\n name: record.name,\n kind: record.kind,\n spanContext: () => spanContext,\n parentSpanContext: record.parentSpanContext ? { ...record.parentSpanContext } : undefined,\n startTime: record.startTime,\n endTime: record.endTime,\n status: record.status,\n attributes: AiContent.withoutAiContent(record.attributes),\n links,\n events: record.events.map((event) => ({\n name: event.name,\n time: event.time,\n attributes: event.attributes,\n droppedAttributesCount: event.droppedAttributesCount,\n })),\n duration: hrTimeDuration(record.startTime, record.endTime),\n ended: true,\n resource: this.#resource,\n instrumentationScope: { ...record.instrumentationScope },\n droppedAttributesCount: record.droppedAttributesCount,\n droppedEventsCount: record.droppedEventsCount,\n droppedLinksCount: record.droppedLinksCount,\n };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"chunk-OtelSpanSink.mjs","names":["TailSampling.TailSampler","TailSampling.hrTimeToMs","AiContent.withoutAiContent"],"sources":["../../src/extensions/otel/ai-content.ts","../../src/extensions/otel/tail-sampling.ts","../../src/extensions/otel/OtelSpanSink.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport { type Attributes, type Context } from '@opentelemetry/api';\nimport { type ReadableSpan, type Span, type SpanProcessor } from '@opentelemetry/sdk-trace-base';\n\nimport * as SpanAttributes from '@dxos/effect/SpanAttributes';\n\n/**\n * The prompt, the response, and the tool names. They stay on the span for the AI analytics sink,\n * which applies the content policy, and never leave the process with it: a trace backend gets the\n * model call's metadata.\n */\nexport const AI_CONTENT_ATTRIBUTES: ReadonlySet<string> = new Set([\n SpanAttributes.AI.input,\n SpanAttributes.AI.output,\n SpanAttributes.AI.tools,\n]);\n\nexport const withoutAiContent = (attributes: Attributes): Attributes =>\n Object.fromEntries(Object.entries(attributes).filter(([key]) => !AI_CONTENT_ATTRIBUTES.has(key)));\n\n/** A view of the span without its content attributes; the span itself is what other processors read. */\nexport const stripAiContent = (span: ReadableSpan): ReadableSpan =>\n Object.create(span, { attributes: { value: withoutAiContent(span.attributes), enumerable: true } });\n\n/** Strips AI content from every span it forwards. Sits directly in front of an exporter. */\nexport class AiContentStrippingSpanProcessor implements SpanProcessor {\n constructor(private readonly _delegate: SpanProcessor) {}\n\n onStart(span: Span, parentContext: Context): void {\n this._delegate.onStart(span, parentContext);\n }\n\n onEnd(span: ReadableSpan): void {\n this._delegate.onEnd(stripAiContent(span));\n }\n\n forceFlush(): Promise<void> {\n return this._delegate.forceFlush();\n }\n\n shutdown(): Promise<void> {\n return this._delegate.shutdown();\n }\n}\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type Attributes, type Context, SpanStatusCode } from '@opentelemetry/api';\nimport type { ReadableSpan, Span, SpanProcessor } from '@opentelemetry/sdk-trace-base';\n\nimport * as SpanAttributes from '@dxos/effect/SpanAttributes';\n\n/**\n * Tail sampling for spans arriving at the observability worker.\n *\n * Head sampling cannot express either rule we want. A sampler runs when a span is *created*, so it\n * cannot know that the span will fail, and it sees the span name before the provider has annotated\n * it. Both decisions are only available once the span has ended — which is what \"tail\" means. The\n * canonical implementation of this is the OpenTelemetry Collector's tail-sampling processor, which\n * buffers whole traces server-side; there is no collector between Composer and SigNoz, so the same\n * decisions are made here instead, on the spans this realm produced.\n *\n * The rules are the canonical tail-sampling set — keep what errored, keep what was slow, sample the\n * rest — as described in OpenTelemetry's own writeup and implemented by the Collector's\n * `tailsamplingprocessor` policies (`status_code`, `latency`, `probabilistic`):\n *\n * - https://opentelemetry.io/blog/2022/tail-sampling/\n * - https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/tailsamplingprocessor\n *\n * Expressing them as a processor that filters in `onEnd` is the documented way to drop spans from a\n * JS SDK, since a `Sampler` cannot: https://github.com/open-telemetry/opentelemetry-js/discussions/2817\n *\n * In order:\n * - a span that **errored** is kept, and its trace is promoted;\n * - a span slower than {@link DEFAULT_SLOW_MS} is kept, and its trace is promoted;\n * - a span marked `dxos.sampling.keep` — a rare flow such as an invitation, which a dashboard counts\n * one by one and a 30% sample would mostly hide — is kept, and its trace is promoted;\n * - a span carrying `gen_ai.*` or `dxos.ai.kind` — a model call, or the turn and tool calls\n * around it that the AI analytics sink reports — is kept, and its trace is promoted, so a model call is never a\n * fraction of the calls that happened. This one is ours rather than canonical: the AI events are\n * what price the product, and a sampled fraction of them reports a fraction of the spend;\n * - any other span of a **promoted** trace is kept;\n * - everything else is kept at {@link DEFAULT_RATIO}, keyed on the trace id so the decision is the\n * same for every span of a trace.\n *\n * Promotion is remembered rather than buffered: holding span payloads until a trace settles would\n * mean an unbounded buffer and a decision delay, and a conversation's trace can stay open for\n * minutes. Remembering only the trace id costs nothing and recovers the case that matters, since a\n * parent ends *after* its children — so promoting on the first error keeps the ancestors that\n * explain it. Siblings that already ended and were dropped are not recovered; that is the price of\n * not buffering, and it is why an errored trace can arrive with gaps rather than whole.\n */\n\n/** Kept fraction for a trace that nothing promoted. Matches the head ratio this replaced. */\nexport const DEFAULT_RATIO = 0.3;\n\n/**\n * Above this, a span is kept as slow.\n *\n * Far above the 5s the Collector's examples use, because this app's slowest spans are model calls\n * and a conversation turn legitimately runs for tens of seconds — a 5s threshold would promote\n * nearly every AI trace and quietly turn the ratio off for exactly the traffic there is most of.\n * At 30s what is left is a hang rather than a slow answer.\n */\nexport const DEFAULT_SLOW_MS = 30_000;\n\n/**\n * Bound on remembered trace ids. Reached only under a burst far above normal span rates, and a trace\n * evicted early degrades to the ratio rather than to a dropped span.\n */\nconst DEFAULT_MAX_TRACKED_TRACES = 10_000;\n\nexport type Options = {\n ratio?: number;\n slowMs?: number;\n maxTrackedTraces?: number;\n};\n\n/** The parts of an ended span the rules read, whether it arrived over a port or ended in-process. */\nexport type Decidable = {\n readonly traceId: string;\n readonly status: { readonly code: SpanStatusCode };\n readonly attributes: Attributes;\n readonly durationMs: number;\n};\n\nexport class TailSampler {\n private readonly _ratio: number;\n private readonly _slowMs: number;\n private readonly _maxTrackedTraces: number;\n /** Insertion-ordered, so the oldest entry is the first key — an LRU without the bookkeeping. */\n private readonly _promoted = new Set<string>();\n\n constructor({\n ratio = DEFAULT_RATIO,\n slowMs = DEFAULT_SLOW_MS,\n maxTrackedTraces = DEFAULT_MAX_TRACKED_TRACES,\n }: Options = {}) {\n this._ratio = ratio;\n this._slowMs = slowMs;\n this._maxTrackedTraces = maxTrackedTraces;\n }\n\n /** Whether the span should be forwarded to the exporter. */\n keep(span: Decidable): boolean {\n if (this._isPromotable(span)) {\n this.promote(span.traceId);\n return true;\n }\n return this._promoted.has(span.traceId) || sampledByTraceId(span.traceId, this._ratio);\n }\n\n private _isPromotable(span: Decidable): boolean {\n return (\n span.status.code === SpanStatusCode.ERROR ||\n span.durationMs > this._slowMs ||\n isKeepMarked(span.attributes) ||\n Object.keys(span.attributes).some(isAiAttribute)\n );\n }\n\n /**\n * Keeps every later span of the trace. Called for what the sampler cannot see itself: a log at\n * warning or above names a trace worth keeping the same way an errored span does.\n */\n promote(traceId: string): void {\n // Re-inserting moves the id to the end, so an active trace is not evicted ahead of a stale one.\n this._promoted.delete(traceId);\n this._promoted.add(traceId);\n while (this._promoted.size > this._maxTrackedTraces) {\n const oldest = this._promoted.values().next();\n if (oldest.done) {\n break;\n }\n this._promoted.delete(oldest.value);\n }\n }\n}\n\nconst GEN_AI_PREFIX = 'gen_ai.';\n\n/** `gen_ai.*` marks a model call; `dxos.ai.kind` the turn around it and the tool calls inside it. */\nconst isAiAttribute = (key: string): boolean => key.startsWith(GEN_AI_PREFIX) || key === SpanAttributes.AI.kind;\n\n/** Manual `@dxos/tracing` spans namespace their attributes under `ctx.`, so the marker arrives either way. */\nconst isKeepMarked = (attributes: Attributes): boolean =>\n attributes[SpanAttributes.SAMPLING.keep] === true || attributes[`ctx.${SpanAttributes.SAMPLING.keep}`] === true;\n\n/**\n * Deterministic per trace, so every span of a trace decides the same way. Reads the low 8 hex digits\n * of the 32-hex-digit trace id — the id is random, so any window is uniform, and 32 bits is well\n * inside the exact range of a double.\n */\nconst sampledByTraceId = (traceId: string, ratio: number): boolean => {\n if (ratio >= 1) {\n return true;\n }\n if (ratio <= 0 || traceId.length < 8) {\n return false;\n }\n const value = Number.parseInt(traceId.slice(-8), 16);\n return Number.isNaN(value) ? false : value / 0x1_0000_0000 < ratio;\n};\n\n/** OTel reports durations as `[seconds, nanoseconds]`. */\nexport const hrTimeToMs = ([seconds, nanos]: [number, number]): number => seconds * 1_000 + nanos / 1e6;\n\n/**\n * Applies the same rules in-process, for the path that exports without a worker (node, and a browser\n * with no observability worker). Without it that path would export everything, since head sampling\n * no longer thins anything.\n */\nexport class TailSamplingSpanProcessor implements SpanProcessor {\n private readonly _sampler: TailSampler;\n\n constructor(\n private readonly _delegate: SpanProcessor,\n options?: Options,\n ) {\n this._sampler = new TailSampler(options);\n }\n\n onStart(span: Span, parentContext: Context): void {\n this._delegate.onStart(span, parentContext);\n }\n\n /** See {@link TailSampler.promote}. */\n promote(traceId: string): void {\n this._sampler.promote(traceId);\n }\n\n onEnd(span: ReadableSpan): void {\n const { traceId } = span.spanContext();\n const decidable = {\n traceId,\n status: span.status,\n attributes: span.attributes,\n durationMs: hrTimeToMs(span.duration),\n };\n if (this._sampler.keep(decidable)) {\n this._delegate.onEnd(span);\n }\n }\n\n forceFlush(): Promise<void> {\n return this._delegate.forceFlush();\n }\n\n shutdown(): Promise<void> {\n return this._delegate.shutdown();\n }\n}\n","//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\n// The log-writer worker imports this through its own subpath, so nothing else loads the\n// OpenTelemetry SDK.\n\nimport {\n type Attributes,\n type Context,\n type HrTime,\n type Link,\n type SpanContext,\n type SpanKind,\n type SpanStatus,\n} from '@opentelemetry/api';\nimport { TraceState, hrTimeDuration } from '@opentelemetry/core';\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';\nimport { type Resource, defaultResource, resourceFromAttributes } from '@opentelemetry/resources';\nimport {\n BatchSpanProcessor,\n type ReadableSpan,\n type Span as SdkSpan,\n type SpanExporter,\n type SpanProcessor,\n} from '@opentelemetry/sdk-trace-base';\n\nimport * as AiContent from './ai-content.ts';\nimport { type OtelDestination, signalUrl } from './otel.ts';\nimport * as TailSampling from './tail-sampling.ts';\n\nexport type Init = {\n type: 'otel-traces-init';\n destinations: OtelDestination[];\n resourceAttributes: Record<string, string>;\n};\n\nexport type Span = {\n type: 'otel-span';\n name: string;\n kind: SpanKind;\n traceId: string;\n spanId: string;\n traceFlags: number;\n traceState?: string;\n parentSpanContext?: { traceId: string; spanId: string; traceFlags: number };\n startTime: HrTime;\n endTime: HrTime;\n status: SpanStatus;\n attributes: Attributes;\n events: { name: string; time: HrTime; attributes?: Attributes; droppedAttributesCount?: number }[];\n links: { context: { traceId: string; spanId: string; traceFlags: number }; attributes?: Attributes }[];\n droppedAttributesCount: number;\n droppedEventsCount: number;\n droppedLinksCount: number;\n instrumentationScope: { name: string; version?: string };\n};\n\nexport type Message = Init | Span;\n\nexport type Handle = { post: (record: Span) => void };\n\nexport const serializeReadableSpan = (span: ReadableSpan): Span => {\n const spanContext = span.spanContext();\n return {\n type: 'otel-span',\n name: span.name,\n kind: span.kind,\n traceId: spanContext.traceId,\n spanId: spanContext.spanId,\n traceFlags: spanContext.traceFlags,\n traceState: spanContext.traceState?.serialize(),\n parentSpanContext: span.parentSpanContext\n ? {\n traceId: span.parentSpanContext.traceId,\n spanId: span.parentSpanContext.spanId,\n traceFlags: span.parentSpanContext.traceFlags,\n }\n : undefined,\n startTime: span.startTime,\n endTime: span.endTime,\n status: span.status,\n attributes: { ...span.attributes },\n events: span.events.map((event) => ({\n name: event.name,\n time: event.time,\n attributes: event.attributes ? { ...event.attributes } : undefined,\n droppedAttributesCount: event.droppedAttributesCount,\n })),\n links: span.links.map((link) => ({\n context: {\n traceId: link.context.traceId,\n spanId: link.context.spanId,\n traceFlags: link.context.traceFlags,\n },\n attributes: link.attributes ? { ...link.attributes } : undefined,\n })),\n droppedAttributesCount: span.droppedAttributesCount,\n droppedEventsCount: span.droppedEventsCount,\n droppedLinksCount: span.droppedLinksCount,\n instrumentationScope: {\n name: span.instrumentationScope.name,\n version: span.instrumentationScope.version,\n },\n };\n};\n\nexport class PortSpanProcessor implements SpanProcessor {\n constructor(private readonly _post: (record: Span) => void) {}\n\n onStart(_span: SdkSpan, _parentContext: Context): void {}\n\n onEnd(span: ReadableSpan): void {\n this._post(serializeReadableSpan(span));\n }\n\n async shutdown(): Promise<void> {}\n\n async forceFlush(): Promise<void> {}\n}\n\nexport type Options = {\n exporter?: SpanExporter;\n /** Overrides the default tail-sampling rules; pass `{ ratio: 1 }` to export everything. */\n sampling?: TailSampling.Options;\n};\n\nexport class Sink {\n readonly #resource: Resource;\n readonly #processors: BatchSpanProcessor[];\n readonly #sampler: TailSampling.TailSampler;\n\n constructor(init: Init, options: Options = {}) {\n this.#sampler = new TailSampling.TailSampler(options.sampling);\n this.#resource = defaultResource().merge(resourceFromAttributes(init.resourceAttributes));\n this.#processors = init.destinations.map(\n (destination) =>\n new BatchSpanProcessor(\n options.exporter ??\n new OTLPTraceExporter({\n url: signalUrl(destination, 'traces'),\n headers: destination.headers,\n concurrencyLimit: 10,\n }),\n ),\n );\n }\n\n /** Keeps the rest of a trace something outside the span stream vouched for, e.g. a warning log. */\n promote(traceId: string): void {\n this.#sampler.promote(traceId);\n }\n\n append(record: Span): void {\n if (\n !this.#sampler.keep({\n traceId: record.traceId,\n status: record.status,\n attributes: record.attributes,\n durationMs: TailSampling.hrTimeToMs(hrTimeDuration(record.startTime, record.endTime)),\n })\n ) {\n return;\n }\n const span = this.#materialize(record);\n for (const processor of this.#processors) {\n processor.onEnd(span);\n }\n }\n\n async flush(): Promise<void> {\n await Promise.all(this.#processors.map((processor) => processor.forceFlush()));\n }\n\n async close(): Promise<void> {\n await Promise.all(this.#processors.map((processor) => processor.shutdown()));\n }\n\n #materialize(record: Span): ReadableSpan {\n const spanContext: SpanContext = {\n traceId: record.traceId,\n spanId: record.spanId,\n traceFlags: record.traceFlags,\n traceState: record.traceState !== undefined ? new TraceState(record.traceState) : undefined,\n };\n const links: Link[] = record.links.map((link) => ({\n context: { ...link.context },\n attributes: link.attributes,\n }));\n return {\n name: record.name,\n kind: record.kind,\n spanContext: () => spanContext,\n parentSpanContext: record.parentSpanContext ? { ...record.parentSpanContext } : undefined,\n startTime: record.startTime,\n endTime: record.endTime,\n status: record.status,\n attributes: AiContent.withoutAiContent(record.attributes),\n links,\n events: record.events.map((event) => ({\n name: event.name,\n time: event.time,\n attributes: event.attributes,\n droppedAttributesCount: event.droppedAttributesCount,\n })),\n duration: hrTimeDuration(record.startTime, record.endTime),\n ended: true,\n resource: this.#resource,\n instrumentationScope: { ...record.instrumentationScope },\n droppedAttributesCount: record.droppedAttributesCount,\n droppedEventsCount: record.droppedEventsCount,\n droppedLinksCount: record.droppedLinksCount,\n };\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"chunk-extensions.mjs","names":[],"sources":["../../src/cli-observability-secrets.json","../../src/extensions/otel/extension.ts","../../src/extensions/otel/index.ts","../../src/extensions/index.ts"],"sourcesContent":["{\n \"POSTHOG_API_KEY\": null,\n \"IPDATA_API_KEY\": null,\n \"OTEL_ENDPOINT\": null,\n \"OTEL_HEADERS\": null\n}","//\n// Copyright 2025 DXOS.org\n//\n\nimport { type Resource, defaultResource, resourceFromAttributes } from '@opentelemetry/resources';\nimport { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';\nimport * as Effect from 'effect/Effect';\nimport * as Match from 'effect/Match';\nimport * as Option from 'effect/Option';\nimport * as Ref from 'effect/Ref';\n\nimport { type Config, getEnvString, resolveTelemetryTag } from '@dxos/config';\nimport { LogLevel, log } from '@dxos/log';\nimport { isNode, isNonNullable } from '@dxos/util';\n\nimport buildSecrets from '../../cli-observability-secrets.json';\nimport * as ObservabilityExtension from '../../ObservabilityExtension.ts';\nimport { getOtelLogLevel, isObservabilityDisabled, storeObservabilityDisabled } from '../../storage/index.ts';\nimport { stubExtension } from '../stub.ts';\nimport { type OtelMetrics } from './metrics.ts';\nimport { type OtelDestination } from './otel.ts';\nimport type * as OtelLogSink from './OtelLogSink.ts';\nimport type * as OtelMetricsSink from './OtelMetricsSink.ts';\nimport type * as OtelSpanSink from './OtelSpanSink.ts';\nimport { RemoteMetricsForwarder } from './remote-metrics.ts';\n\nexport type OtelWorkerMessage = OtelLogSink.Message | OtelMetricsSink.Message | OtelSpanSink.Message;\n\n/** The OTEL extension always implements `close`, unlike the general (optional) `Extension` API. */\ntype OtelExtension = ObservabilityExtension.Extension & { close(): Effect.Effect<void> };\n\n/** One-way send handle into the observability worker. */\nexport type OtelWorkerPort = { post: (message: OtelWorkerMessage) => void };\n\nexport type ExtensionsOptions = {\n /** For the OTEL, the name of the entity for which signals (metrics or trace) are collected. */\n serviceName: string;\n /** For the OTEL, the version of the entity for which signals (metrics or trace) are collected. */\n serviceVersion: string;\n /** For the OTEL, the environment of the entity for which signals (metrics or trace) are collected. */\n environment: string;\n /** Where the user's opt-out is stored: a localForage prefix in the browser, a config directory in node. */\n namespace?: string;\n config: Config;\n endpoint?: string;\n headers?: Record<string, string>;\n additionalDestinations?: OtelDestination[];\n logs?: boolean;\n /** Minimum log level to export. Defaults to INFO (i.e. info, warn, error). */\n logLevel?: LogLevel;\n observabilityWorker?: OtelWorkerPort;\n metrics?: boolean;\n traces?: boolean;\n};\n\n/** Create an OTEL-backed observability extension for logs, metrics, and/or traces. */\nexport const extensions: (options: ExtensionsOptions) => Effect.Effect<ObservabilityExtension.Extension> = Effect.fn(\n function* ({\n serviceName,\n serviceVersion,\n environment,\n namespace = serviceName,\n config,\n endpoint: _endpoint,\n headers: _headers,\n additionalDestinations = [],\n // TODO(wittjosiah): Logging integration.\n // - logger should run even if observability is disabled\n // - logs should be cached locally in a circular buffer\n // - logs should be flushed to the server if user opts to include them in a bug report\n logs: logsEnabled = false,\n logLevel = LogLevel.INFO,\n observabilityWorker,\n metrics: metricsEnabled = false,\n traces: tracesEnabled = false,\n }) {\n const { OtelLogs } = yield* Effect.promise(() => import('./logs.ts'));\n const { OtelMetrics } = yield* Effect.promise(() => import('./metrics.ts'));\n const { OtelTraces } = yield* Effect.promise(() => import('#otel-traces'));\n\n const cachedDisabled = yield* Effect.promise(() => isObservabilityDisabled(namespace));\n const disabled = cachedDisabled || isObservabilityDisabledSync(namespace);\n const storedLogLevel = yield* Effect.promise(() => getOtelLogLevel(namespace));\n const resolvedLogLevel =\n storedLogLevel != null ? (LogLevel[storedLogLevel.toUpperCase() as keyof typeof LogLevel] ?? logLevel) : logLevel;\n const enabledRef = yield* Ref.make(!disabled);\n const tags = new Map<string, string>();\n // Tags for log records only. Kept apart from `tags` because those also land on spans and on\n // metric attributes, where a per-session value would explode cardinality.\n const logTags = new Map<string, string>();\n\n const rawEndpoint = isNode()\n ? (process.env.DX_OTEL_ENDPOINT ?? _endpoint ?? buildSecrets.OTEL_ENDPOINT)\n : (getEnvString(config, 'DX_OTEL_ENDPOINT') ?? _endpoint);\n // The OTLP exporter (>= 0.203) validates URLs and rejects relative paths.\n // In the browser/worker, resolve relative endpoints against the current origin\n // so callers can keep using paths like `/api/otel` for proxied deployments.\n const endpoint = !isNode() && rawEndpoint?.startsWith('/') ? resolveRelativeEndpoint(rawEndpoint) : rawEndpoint;\n const headers =\n _headers ??\n Match.value(isNode()).pipe(\n Match.when(true, () => Option.fromNullishOr(process.env.DX_OTEL_HEADERS ?? buildSecrets.OTEL_HEADERS)),\n Match.when(false, () => Option.fromNullishOr(getEnvString(config, 'DX_OTEL_HEADERS'))),\n Match.exhaustive,\n Option.map((raw) => parseHeaders(raw)),\n Option.getOrElse(() => undefined),\n );\n\n if (!endpoint) {\n log.info('Missing OTEL_ENDPOINT');\n }\n\n const destinations: OtelDestination[] = [\n ...(endpoint ? [{ endpoint, headers: headers ?? {} }] : []),\n ...additionalDestinations,\n ];\n if (destinations.length === 0) {\n return stubExtension;\n }\n log.info('otel destinations', { destinations: destinations.map(({ endpoint }) => endpoint) });\n\n // Matches edge's `ctx.tag` span attribute (stamped by the edge log middleware\n // when it reads the `X-DXOS-Client-Tag` header, see\n // /edge/packages/services/edge/src/log-middleware.ts).\n //\n // Stamped in TWO places:\n // 1. Resource attribute — so logs and metrics emitted from this extension\n // also carry it, and for zero per-span cost.\n // 2. Span attribute via `tags` — because edge puts `ctx.tag` in the span-\n // attribute context (not resource), and SigNoz indexes the two contexts\n // separately. Stamping it on span attributes here keeps a single\n // `ctx.tag = <value>` filter matching spans from both tiers in SigNoz\n // without requiring qualified `attribute.ctx.tag`/`resource.ctx.tag` syntax.\n const clientTag = resolveTelemetryTag(config);\n if (clientTag) {\n tags.set('ctx.tag', clientTag);\n }\n\n // `service.version` is the SDK constant and cannot separate two deploys of one release.\n // Constant per build, so it adds no concurrent metric series. The semconv key `vcs.ref.head.revision` is\n // inlined to keep the 14k-export `semantic-conventions/incubating` module out of the bundle.\n const commitHash = config.get('runtime.app.build.commitHash');\n\n const baseAttributes = {\n [ATTR_SERVICE_NAME]: serviceName,\n [ATTR_SERVICE_VERSION]: serviceVersion,\n 'deployment.environment': environment,\n 'dxos.process.type': detectProcessType(),\n ...(clientTag ? { 'ctx.tag': clientTag } : {}),\n ...(commitHash ? { 'vcs.ref.head.revision': commitHash } : {}),\n };\n const sessionId = crypto.randomUUID();\n const { resource, metricsResource } = createResources(baseAttributes, sessionId);\n\n const traces =\n tracesEnabled && !disabled\n ? new OtelTraces({\n destinations,\n resource,\n getTags: () => Object.fromEntries(tags),\n spanSink: observabilityWorker\n ? { post: (record: OtelSpanSink.Span) => observabilityWorker.post(record) }\n : undefined,\n })\n : undefined;\n const remoteLogs = logsEnabled && !disabled ? observabilityWorker : undefined;\n const logs =\n logsEnabled && !disabled && !remoteLogs\n ? new OtelLogs({\n destinations,\n resource,\n getTags: () => ({ ...Object.fromEntries(tags), ...Object.fromEntries(logTags) }),\n logLevel: resolvedLogLevel,\n onTraceFlagged: (traceId) => traces?.promote(traceId),\n })\n : undefined;\n\n const remoteMetrics =\n metricsEnabled && observabilityWorker && !disabled\n ? new RemoteMetricsForwarder(observabilityWorker.post)\n : undefined;\n const metrics =\n metricsEnabled && !disabled && !observabilityWorker\n ? new OtelMetrics({\n destinations,\n resource: metricsResource,\n getTags: () => Object.fromEntries(tags),\n })\n : undefined;\n\n const extension: OtelExtension = {\n initialize: () =>\n Effect.sync(() => {\n if (logs) {\n log.runtimeConfig.processors.push(logs.logProcessor);\n }\n if (remoteLogs) {\n remoteLogs.post({\n type: 'otel-init',\n destinations,\n resourceAttributes: { ...baseAttributes, 'session.id': sessionId },\n logLevel: resolvedLogLevel,\n tags: Object.fromEntries(tags),\n });\n }\n if (remoteMetrics) {\n observabilityWorker?.post({\n type: 'otel-metrics-init',\n destinations,\n resourceAttributes: baseAttributes,\n tags: Object.fromEntries(tags),\n });\n }\n if (traces) {\n if (observabilityWorker) {\n observabilityWorker.post({\n type: 'otel-traces-init',\n destinations,\n resourceAttributes: { ...baseAttributes, 'session.id': sessionId },\n });\n }\n traces.start();\n }\n }),\n enable: Effect.fn(function* () {\n yield* Effect.promise(() => storeObservabilityDisabled(namespace, false));\n yield* Ref.update(enabledRef, () => true);\n }),\n // Ordered so nothing is captured after this point: the processor does not consult\n // `enabledRef`, and the store is awaited before a shutdown that may throw.\n disable: Effect.fn(function* () {\n yield* Ref.update(enabledRef, () => false);\n if (logs) {\n const index = log.runtimeConfig.processors.indexOf(logs.logProcessor);\n if (index >= 0) {\n log.runtimeConfig.processors.splice(index, 1);\n }\n }\n yield* Effect.promise(() => storeObservabilityDisabled(namespace, true));\n // TODO(wittjosiah): `close` drains by design, so revoking still ships what is queued;\n // discarding it means shutting each exporter down before its provider.\n yield* extension.close();\n }),\n close: () =>\n Effect.promise(async () => {\n remoteMetrics?.flush();\n remoteLogs?.post({ type: 'otel-flush' });\n // Run logs/metrics close concurrently and swallow their failures so the\n // tracer provider shutdown below ALWAYS runs. Without this, a rejection\n // from logs or metrics would drop the tracer provider's BatchSpanProcessor\n // queue on process exit, manifesting as \"Missing Span\" in SigNoz for any\n // already-exported children.\n const results = await Promise.allSettled([logs?.close(), metrics?.close(), remoteMetrics?.close()]);\n for (const result of results) {\n if (result.status === 'rejected') {\n log.catch(result.reason);\n }\n }\n // Critical: shut down the tracer provider so BatchSpanProcessor drains its\n // queue. Otherwise spans enqueued in the last 5s (close/teardown spans)\n // are dropped on process exit.\n await traces?.close();\n }),\n flush: () =>\n Effect.promise(async () => {\n remoteMetrics?.flush();\n remoteLogs?.post({ type: 'otel-flush' });\n const results = await Promise.allSettled([logs?.flush(), metrics?.flush()]);\n for (const result of results) {\n if (result.status === 'rejected') {\n log.catch(result.reason);\n }\n }\n await traces?.flush();\n }),\n setTags: (incomingTags, kind) => {\n const target = kind === 'logs' ? logTags : tags;\n for (const [key, value] of Object.entries(incomingTags)) {\n target.set(key, value);\n }\n remoteLogs?.post({ type: 'otel-tags', tags: { ...incomingTags } });\n },\n get enabled() {\n return Ref.get(enabledRef).pipe(Effect.runSync);\n },\n apis: [\n {\n kind: 'logs',\n isAvailable: () => Effect.succeed(!!logs || !!remoteLogs),\n } satisfies ObservabilityExtension.ExtensionApi,\n metricsApi(metrics ?? remoteMetrics),\n traces\n ? ({ kind: 'traces', isAvailable: () => Effect.succeed(true) } satisfies ObservabilityExtension.ExtensionApi)\n : undefined,\n ].filter(isNonNullable),\n };\n\n return extension;\n },\n);\n\nconst metricsApi = (\n metrics: OtelMetrics | RemoteMetricsForwarder | undefined,\n): ObservabilityExtension.ExtensionApi | undefined =>\n metrics\n ? ({\n kind: 'metrics',\n isAvailable: () => Effect.succeed(true),\n gauge: (name, value, tags, meta) => metrics.gauge(name, value, tags, meta),\n increment: (name, value, tags, meta) => metrics.increment(name, value, tags, meta),\n distribution: (name, value, tags, meta) => metrics.distribution(name, value, tags, meta),\n observe: (name, callback, tags, meta) => metrics.observe(name, callback, tags, meta),\n } satisfies ObservabilityExtension.ExtensionApi)\n : undefined;\n\n/**\n * Builds the resource for logs/traces and the separate one for metrics.\n * Metrics omit `session.id` because a per-page-load attribute mints a new time series on every\n * reload. A separate resource is the only option: views reach datapoint attributes, not resource ones.\n */\nexport const createResources = (\n attributes: Record<string, string>,\n sessionId: string,\n): { resource: Resource; metricsResource: Resource } => ({\n resource: defaultResource().merge(resourceFromAttributes({ ...attributes, 'session.id': sessionId })),\n metricsResource: defaultResource().merge(resourceFromAttributes(attributes)),\n});\n\n/**\n * Synchronous best-effort check for observability opt-out.\n * Prevents telemetry from being emitted before the async storage check completes.\n */\nconst isObservabilityDisabledSync = (serviceName: string): boolean => {\n if (isNode()) {\n return process.env.DX_DISABLE_OBSERVABILITY === 'true';\n }\n try {\n if (typeof localStorage !== 'undefined') {\n return localStorage.getItem(`${serviceName}/observability-disabled`) === 'true';\n }\n } catch {\n // localStorage not available (e.g., in workers).\n }\n return false;\n};\n\n/** Best-effort detection of the JavaScript execution context type. */\nconst detectProcessType = (): string => {\n if (isNode()) {\n return 'node';\n }\n if (typeof window !== 'undefined') {\n return 'browser';\n }\n if (typeof (globalThis as any).ServiceWorkerGlobalScope !== 'undefined') {\n return 'service-worker';\n }\n if (typeof (globalThis as any).SharedWorkerGlobalScope !== 'undefined') {\n return 'shared-worker';\n }\n return 'dedicated-worker';\n};\n\n/**\n * Resolves a relative OTLP endpoint path against the current global origin.\n * Workers expose the same `globalThis.location` as the page that spawned them;\n * if neither is available we fall back to the original path and let the OTLP\n * exporter surface the configuration error.\n */\nconst resolveRelativeEndpoint = (path: string): string => {\n const origin = (globalThis as { location?: { origin?: string } }).location?.origin;\n return origin ? `${origin}${path}` : path;\n};\n\nconst parseHeaders = (unparsedHeaders: string): Record<string, string> => {\n return unparsedHeaders.split(';').reduce((acc: Record<string, string>, header) => {\n const [key, ...rest] = header.split(':');\n if (key && rest.length > 0) {\n acc[key.trim().toLowerCase()] = rest.join(':').trim();\n }\n\n return acc;\n }, {});\n};\n","//\n// Copyright 2024 DXOS.org\n//\n\nexport * from './extension.ts';\nexport { addSpanProcessor } from './span-fanout.ts';\nexport { activeTraceContext } from './trace-context.ts';\n","//\n// Copyright 2025 DXOS.org\n//\n\nexport * as Otel from './otel/index.ts';\nexport * as PostHog from './posthog/index.ts';\n"],"mappings":"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"}
1
+ {"version":3,"file":"chunk-extensions.mjs","names":["isObservabilityDisabled","getOtelLogLevel","buildSecrets","storeObservabilityDisabled"],"sources":["../../src/cli-observability-secrets.json","../../src/extensions/otel/extension.ts","../../src/extensions/otel/index.ts","../../src/extensions/index.ts"],"sourcesContent":["{\n \"POSTHOG_API_KEY\": null,\n \"IPDATA_API_KEY\": null,\n \"OTEL_ENDPOINT\": null,\n \"OTEL_HEADERS\": null\n}","//\n// Copyright 2025 DXOS.org\n//\n\nimport { type Resource, defaultResource, resourceFromAttributes } from '@opentelemetry/resources';\nimport { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';\nimport * as Effect from 'effect/Effect';\nimport * as Match from 'effect/Match';\nimport * as Option from 'effect/Option';\nimport * as Ref from 'effect/Ref';\n\nimport { type Config, getEnvString, resolveTelemetryTag } from '@dxos/config';\nimport { LogLevel, log } from '@dxos/log';\nimport { isNode, isNonNullable } from '@dxos/util';\n\nimport buildSecrets from '../../cli-observability-secrets.json';\nimport * as ObservabilityExtension from '../../ObservabilityExtension.ts';\nimport { getOtelLogLevel, isObservabilityDisabled, storeObservabilityDisabled } from '../../storage/index.ts';\nimport { stubExtension } from '../stub.ts';\nimport { type OtelMetrics } from './metrics.ts';\nimport { type OtelDestination } from './otel.ts';\nimport type * as OtelLogSink from './OtelLogSink.ts';\nimport type * as OtelMetricsSink from './OtelMetricsSink.ts';\nimport type * as OtelSpanSink from './OtelSpanSink.ts';\nimport { RemoteMetricsForwarder } from './remote-metrics.ts';\n\nexport type OtelWorkerMessage = OtelLogSink.Message | OtelMetricsSink.Message | OtelSpanSink.Message;\n\n/** The OTEL extension always implements `close`, unlike the general (optional) `Extension` API. */\ntype OtelExtension = ObservabilityExtension.Extension & { close(): Effect.Effect<void> };\n\n/** One-way send handle into the observability worker. */\nexport type OtelWorkerPort = { post: (message: OtelWorkerMessage) => void };\n\nexport type ExtensionsOptions = {\n /** For the OTEL, the name of the entity for which signals (metrics or trace) are collected. */\n serviceName: string;\n /** For the OTEL, the version of the entity for which signals (metrics or trace) are collected. */\n serviceVersion: string;\n /** For the OTEL, the environment of the entity for which signals (metrics or trace) are collected. */\n environment: string;\n /** Where the user's opt-out is stored: a localForage prefix in the browser, a config directory in node. */\n namespace?: string;\n config: Config;\n endpoint?: string;\n headers?: Record<string, string>;\n additionalDestinations?: OtelDestination[];\n logs?: boolean;\n /** Minimum log level to export. Defaults to INFO (i.e. info, warn, error). */\n logLevel?: LogLevel;\n observabilityWorker?: OtelWorkerPort;\n metrics?: boolean;\n traces?: boolean;\n};\n\n/** Create an OTEL-backed observability extension for logs, metrics, and/or traces. */\nexport const extensions: (options: ExtensionsOptions) => Effect.Effect<ObservabilityExtension.Extension> = Effect.fn(\n function* ({\n serviceName,\n serviceVersion,\n environment,\n namespace = serviceName,\n config,\n endpoint: _endpoint,\n headers: _headers,\n additionalDestinations = [],\n // TODO(wittjosiah): Logging integration.\n // - logger should run even if observability is disabled\n // - logs should be cached locally in a circular buffer\n // - logs should be flushed to the server if user opts to include them in a bug report\n logs: logsEnabled = false,\n logLevel = LogLevel.INFO,\n observabilityWorker,\n metrics: metricsEnabled = false,\n traces: tracesEnabled = false,\n }) {\n const { OtelLogs } = yield* Effect.promise(() => import('./logs.ts'));\n const { OtelMetrics } = yield* Effect.promise(() => import('./metrics.ts'));\n const { OtelTraces } = yield* Effect.promise(() => import('#otel-traces'));\n\n const cachedDisabled = yield* Effect.promise(() => isObservabilityDisabled(namespace));\n const disabled = cachedDisabled || isObservabilityDisabledSync(namespace);\n const storedLogLevel = yield* Effect.promise(() => getOtelLogLevel(namespace));\n const resolvedLogLevel =\n storedLogLevel != null ? (LogLevel[storedLogLevel.toUpperCase() as keyof typeof LogLevel] ?? logLevel) : logLevel;\n const enabledRef = yield* Ref.make(!disabled);\n const tags = new Map<string, string>();\n // Tags for log records only. Kept apart from `tags` because those also land on spans and on\n // metric attributes, where a per-session value would explode cardinality.\n const logTags = new Map<string, string>();\n\n const rawEndpoint = isNode()\n ? (process.env.DX_OTEL_ENDPOINT ?? _endpoint ?? buildSecrets.OTEL_ENDPOINT)\n : (getEnvString(config, 'DX_OTEL_ENDPOINT') ?? _endpoint);\n // The OTLP exporter (>= 0.203) validates URLs and rejects relative paths.\n // In the browser/worker, resolve relative endpoints against the current origin\n // so callers can keep using paths like `/api/otel` for proxied deployments.\n const endpoint = !isNode() && rawEndpoint?.startsWith('/') ? resolveRelativeEndpoint(rawEndpoint) : rawEndpoint;\n const headers =\n _headers ??\n Match.value(isNode()).pipe(\n Match.when(true, () => Option.fromNullishOr(process.env.DX_OTEL_HEADERS ?? buildSecrets.OTEL_HEADERS)),\n Match.when(false, () => Option.fromNullishOr(getEnvString(config, 'DX_OTEL_HEADERS'))),\n Match.exhaustive,\n Option.map((raw) => parseHeaders(raw)),\n Option.getOrElse(() => undefined),\n );\n\n if (!endpoint) {\n log.info('Missing OTEL_ENDPOINT');\n }\n\n const destinations: OtelDestination[] = [\n ...(endpoint ? [{ endpoint, headers: headers ?? {} }] : []),\n ...additionalDestinations,\n ];\n if (destinations.length === 0) {\n return stubExtension;\n }\n log.info('otel destinations', { destinations: destinations.map(({ endpoint }) => endpoint) });\n\n // Matches edge's `ctx.tag` span attribute (stamped by the edge log middleware\n // when it reads the `X-DXOS-Client-Tag` header, see\n // /edge/packages/services/edge/src/log-middleware.ts).\n //\n // Stamped in TWO places:\n // 1. Resource attribute — so logs and metrics emitted from this extension\n // also carry it, and for zero per-span cost.\n // 2. Span attribute via `tags` — because edge puts `ctx.tag` in the span-\n // attribute context (not resource), and SigNoz indexes the two contexts\n // separately. Stamping it on span attributes here keeps a single\n // `ctx.tag = <value>` filter matching spans from both tiers in SigNoz\n // without requiring qualified `attribute.ctx.tag`/`resource.ctx.tag` syntax.\n const clientTag = resolveTelemetryTag(config);\n if (clientTag) {\n tags.set('ctx.tag', clientTag);\n }\n\n // `service.version` is the SDK constant and cannot separate two deploys of one release.\n // Constant per build, so it adds no concurrent metric series. The semconv key `vcs.ref.head.revision` is\n // inlined to keep the 14k-export `semantic-conventions/incubating` module out of the bundle.\n const commitHash = config.get('runtime.app.build.commitHash');\n\n const baseAttributes = {\n [ATTR_SERVICE_NAME]: serviceName,\n [ATTR_SERVICE_VERSION]: serviceVersion,\n 'deployment.environment': environment,\n 'dxos.process.type': detectProcessType(),\n ...(clientTag ? { 'ctx.tag': clientTag } : {}),\n ...(commitHash ? { 'vcs.ref.head.revision': commitHash } : {}),\n };\n const sessionId = crypto.randomUUID();\n const { resource, metricsResource } = createResources(baseAttributes, sessionId);\n\n const traces =\n tracesEnabled && !disabled\n ? new OtelTraces({\n destinations,\n resource,\n getTags: () => Object.fromEntries(tags),\n spanSink: observabilityWorker\n ? { post: (record: OtelSpanSink.Span) => observabilityWorker.post(record) }\n : undefined,\n })\n : undefined;\n const remoteLogs = logsEnabled && !disabled ? observabilityWorker : undefined;\n const logs =\n logsEnabled && !disabled && !remoteLogs\n ? new OtelLogs({\n destinations,\n resource,\n getTags: () => ({ ...Object.fromEntries(tags), ...Object.fromEntries(logTags) }),\n logLevel: resolvedLogLevel,\n onTraceFlagged: (traceId) => traces?.promote(traceId),\n })\n : undefined;\n\n const remoteMetrics =\n metricsEnabled && observabilityWorker && !disabled\n ? new RemoteMetricsForwarder(observabilityWorker.post)\n : undefined;\n const metrics =\n metricsEnabled && !disabled && !observabilityWorker\n ? new OtelMetrics({\n destinations,\n resource: metricsResource,\n getTags: () => Object.fromEntries(tags),\n })\n : undefined;\n\n const extension: OtelExtension = {\n initialize: () =>\n Effect.sync(() => {\n if (logs) {\n log.runtimeConfig.processors.push(logs.logProcessor);\n }\n if (remoteLogs) {\n remoteLogs.post({\n type: 'otel-init',\n destinations,\n resourceAttributes: { ...baseAttributes, 'session.id': sessionId },\n logLevel: resolvedLogLevel,\n tags: Object.fromEntries(tags),\n });\n }\n if (remoteMetrics) {\n observabilityWorker?.post({\n type: 'otel-metrics-init',\n destinations,\n resourceAttributes: baseAttributes,\n tags: Object.fromEntries(tags),\n });\n }\n if (traces) {\n if (observabilityWorker) {\n observabilityWorker.post({\n type: 'otel-traces-init',\n destinations,\n resourceAttributes: { ...baseAttributes, 'session.id': sessionId },\n });\n }\n traces.start();\n }\n }),\n enable: Effect.fn(function* () {\n yield* Effect.promise(() => storeObservabilityDisabled(namespace, false));\n yield* Ref.update(enabledRef, () => true);\n }),\n // Ordered so nothing is captured after this point: the processor does not consult\n // `enabledRef`, and the store is awaited before a shutdown that may throw.\n disable: Effect.fn(function* () {\n yield* Ref.update(enabledRef, () => false);\n if (logs) {\n const index = log.runtimeConfig.processors.indexOf(logs.logProcessor);\n if (index >= 0) {\n log.runtimeConfig.processors.splice(index, 1);\n }\n }\n yield* Effect.promise(() => storeObservabilityDisabled(namespace, true));\n // TODO(wittjosiah): `close` drains by design, so revoking still ships what is queued;\n // discarding it means shutting each exporter down before its provider.\n yield* extension.close();\n }),\n close: () =>\n Effect.promise(async () => {\n remoteMetrics?.flush();\n remoteLogs?.post({ type: 'otel-flush' });\n // Run logs/metrics close concurrently and swallow their failures so the\n // tracer provider shutdown below ALWAYS runs. Without this, a rejection\n // from logs or metrics would drop the tracer provider's BatchSpanProcessor\n // queue on process exit, manifesting as \"Missing Span\" in SigNoz for any\n // already-exported children.\n const results = await Promise.allSettled([logs?.close(), metrics?.close(), remoteMetrics?.close()]);\n for (const result of results) {\n if (result.status === 'rejected') {\n log.catch(result.reason);\n }\n }\n // Critical: shut down the tracer provider so BatchSpanProcessor drains its\n // queue. Otherwise spans enqueued in the last 5s (close/teardown spans)\n // are dropped on process exit.\n await traces?.close();\n }),\n flush: () =>\n Effect.promise(async () => {\n remoteMetrics?.flush();\n remoteLogs?.post({ type: 'otel-flush' });\n const results = await Promise.allSettled([logs?.flush(), metrics?.flush()]);\n for (const result of results) {\n if (result.status === 'rejected') {\n log.catch(result.reason);\n }\n }\n await traces?.flush();\n }),\n setTags: (incomingTags, kind) => {\n const target = kind === 'logs' ? logTags : tags;\n for (const [key, value] of Object.entries(incomingTags)) {\n target.set(key, value);\n }\n remoteLogs?.post({ type: 'otel-tags', tags: { ...incomingTags } });\n },\n get enabled() {\n return Ref.get(enabledRef).pipe(Effect.runSync);\n },\n apis: [\n {\n kind: 'logs',\n isAvailable: () => Effect.succeed(!!logs || !!remoteLogs),\n } satisfies ObservabilityExtension.ExtensionApi,\n metricsApi(metrics ?? remoteMetrics),\n traces\n ? ({ kind: 'traces', isAvailable: () => Effect.succeed(true) } satisfies ObservabilityExtension.ExtensionApi)\n : undefined,\n ].filter(isNonNullable),\n };\n\n return extension;\n },\n);\n\nconst metricsApi = (\n metrics: OtelMetrics | RemoteMetricsForwarder | undefined,\n): ObservabilityExtension.ExtensionApi | undefined =>\n metrics\n ? ({\n kind: 'metrics',\n isAvailable: () => Effect.succeed(true),\n gauge: (name, value, tags, meta) => metrics.gauge(name, value, tags, meta),\n increment: (name, value, tags, meta) => metrics.increment(name, value, tags, meta),\n distribution: (name, value, tags, meta) => metrics.distribution(name, value, tags, meta),\n observe: (name, callback, tags, meta) => metrics.observe(name, callback, tags, meta),\n } satisfies ObservabilityExtension.ExtensionApi)\n : undefined;\n\n/**\n * Builds the resource for logs/traces and the separate one for metrics.\n * Metrics omit `session.id` because a per-page-load attribute mints a new time series on every\n * reload. A separate resource is the only option: views reach datapoint attributes, not resource ones.\n */\nexport const createResources = (\n attributes: Record<string, string>,\n sessionId: string,\n): { resource: Resource; metricsResource: Resource } => ({\n resource: defaultResource().merge(resourceFromAttributes({ ...attributes, 'session.id': sessionId })),\n metricsResource: defaultResource().merge(resourceFromAttributes(attributes)),\n});\n\n/**\n * Synchronous best-effort check for observability opt-out.\n * Prevents telemetry from being emitted before the async storage check completes.\n */\nconst isObservabilityDisabledSync = (serviceName: string): boolean => {\n if (isNode()) {\n return process.env.DX_DISABLE_OBSERVABILITY === 'true';\n }\n try {\n if (typeof localStorage !== 'undefined') {\n return localStorage.getItem(`${serviceName}/observability-disabled`) === 'true';\n }\n } catch {\n // localStorage not available (e.g., in workers).\n }\n return false;\n};\n\n/** Best-effort detection of the JavaScript execution context type. */\nconst detectProcessType = (): string => {\n if (isNode()) {\n return 'node';\n }\n if (typeof window !== 'undefined') {\n return 'browser';\n }\n if (typeof (globalThis as any).ServiceWorkerGlobalScope !== 'undefined') {\n return 'service-worker';\n }\n if (typeof (globalThis as any).SharedWorkerGlobalScope !== 'undefined') {\n return 'shared-worker';\n }\n return 'dedicated-worker';\n};\n\n/**\n * Resolves a relative OTLP endpoint path against the current global origin.\n * Workers expose the same `globalThis.location` as the page that spawned them;\n * if neither is available we fall back to the original path and let the OTLP\n * exporter surface the configuration error.\n */\nconst resolveRelativeEndpoint = (path: string): string => {\n const origin = (globalThis as { location?: { origin?: string } }).location?.origin;\n return origin ? `${origin}${path}` : path;\n};\n\nconst parseHeaders = (unparsedHeaders: string): Record<string, string> => {\n return unparsedHeaders.split(';').reduce((acc: Record<string, string>, header) => {\n const [key, ...rest] = header.split(':');\n if (key && rest.length > 0) {\n acc[key.trim().toLowerCase()] = rest.join(':').trim();\n }\n\n return acc;\n }, {});\n};\n","//\n// Copyright 2024 DXOS.org\n//\n\nexport * from './extension.ts';\nexport { addSpanProcessor } from './span-fanout.ts';\nexport { activeTraceContext } from './trace-context.ts';\n","//\n// Copyright 2025 DXOS.org\n//\n\nexport * as Otel from './otel/index.ts';\nexport * as PostHog from './posthog/index.ts';\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"chunk-logs.mjs","names":[],"sources":["../../src/extensions/otel/logs.ts"],"sourcesContent":["//\n// Copyright 2024 DXOS.org\n//\n\nimport { type Context, context as otelContext, trace } from '@opentelemetry/api';\nimport { type AnyValueMap, SeverityNumber } from '@opentelemetry/api-logs';\nimport { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';\nimport { BatchLogRecordProcessor, LoggerProvider, type LogRecordExporter } from '@opentelemetry/sdk-logs';\nimport { ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';\n\nimport {\n type LogConfig,\n type LogEntry,\n LogLevel,\n type LogProcessor,\n getContextFromEntry,\n getRelativeFilename,\n} from '@dxos/log';\n\nimport { type OtelOptions, setDiagLogger, signalUrl } from './otel.ts';\n\nconst FLATTEN_DEPTH = 1;\n\nexport type OtelLogOptions = OtelOptions & {\n logLevel: LogLevel;\n exporter?: LogRecordExporter;\n /**\n * Batch processor sizing. The defaults (queue 2048, batches of 512) suit a live stream; a\n * one-shot dump of a whole log buffer must raise them or the processor drops records.\n */\n batch?: { maxQueueSize?: number; maxExportBatchSize?: number };\n /**\n * Called with the trace id of every entry at warning or above emitted inside a span, before the\n * export level is applied, so the tail sampler can keep that trace.\n */\n onTraceFlagged?: (traceId: string) => void;\n /**\n * Include logs forwarded from the shared worker via LoggingService.\n *\n * @deprecated\n */\n includeSharedWorkerLogs?: boolean;\n};\n\nexport class OtelLogs {\n private _loggerProvider: LoggerProvider;\n constructor(private readonly options: OtelLogOptions) {\n setDiagLogger(options.consoleDiagLogLevel);\n this._loggerProvider = new LoggerProvider({\n resource: this.options.resource,\n processors: this.options.destinations.map(\n (destination) =>\n new BatchLogRecordProcessor({\n exporter:\n options.exporter ??\n new OTLPLogExporter({\n url: signalUrl(destination, 'logs'),\n headers: destination.headers,\n concurrencyLimit: 10,\n }),\n ...options.batch,\n }),\n ),\n });\n }\n\n public readonly logProcessor: LogProcessor = (_config: LogConfig, entry: LogEntry) => {\n if (entry.level >= LogLevel.WARN && this.options.onTraceFlagged) {\n const traceId = trace.getSpan(otelContext.active())?.spanContext().traceId;\n if (traceId !== undefined) {\n this.options.onTraceFlagged(traceId);\n }\n }\n if (\n entry.level < this.options.logLevel ||\n (!this.options.includeSharedWorkerLogs && entry.meta?.S?.remoteSessionId)\n ) {\n return;\n }\n\n this.emit({\n severityNumber: convertLevel(entry.level),\n body: entry.message,\n timestamp: new Date(entry.timestamp),\n attributes: {\n ...(entry.meta ? { meta: { file: getRelativeFilename(entry.meta.F), line: entry.meta.L } } : {}),\n ...(entry.error ? { error: entry.error.stack } : {}),\n ...stringifyValues(getContextFromEntry(entry), 'ctx_'),\n },\n context: otelContext.active(),\n });\n };\n\n emit(record: {\n severityNumber: SeverityNumber;\n body?: string;\n timestamp: Date;\n attributes: AnyValueMap;\n /** Context whose span the record links to; absent for a record with no trace. */\n context?: Context;\n }): void {\n const logger = this._loggerProvider.getLogger(\n 'dxos-observability',\n this.options.resource.attributes[ATTR_SERVICE_VERSION]?.toString(),\n );\n\n logger.emit({\n severityNumber: record.severityNumber,\n body: record.body,\n timestamp: record.timestamp,\n attributes: { ...this.options.getTags(), ...record.attributes },\n context: record.context,\n });\n }\n\n flush(): Promise<void> {\n return this._loggerProvider.forceFlush();\n }\n\n close(): Promise<void> {\n return this._loggerProvider.shutdown();\n }\n}\n\n/** Maps {@link LogLevel} to OpenTelemetry {@link SeverityNumber}. */\nexport const convertLevel = (level: LogLevel): SeverityNumber => {\n switch (level) {\n case LogLevel.TRACE:\n return SeverityNumber.TRACE;\n case LogLevel.DEBUG:\n return SeverityNumber.DEBUG;\n case LogLevel.VERBOSE:\n return SeverityNumber.INFO;\n case LogLevel.INFO:\n return SeverityNumber.INFO;\n case LogLevel.WARN:\n return SeverityNumber.WARN;\n case LogLevel.ERROR:\n return SeverityNumber.ERROR;\n default:\n return SeverityNumber.UNSPECIFIED;\n }\n};\n\nconst errorToString = (error: Error): string => error.stack ?? `${error.name}: ${error.message}`;\n\nconst safeStringify = (value: unknown): string => {\n try {\n // Replace errors as they are encountered; `JSON.stringify` alone renders them as `{}`.\n return JSON.stringify(value, (_key, nested) => (nested instanceof Error ? errorToString(nested) : nested));\n } catch {\n return '[Circular]';\n }\n};\n\n// TODO(wittjosiah): Reconcile logging utils w/ EDGE.\nexport const stringifyValues = (object: object | undefined, keyPrefix?: string, depth: number = 1) => {\n if (!object) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(object)) {\n if (value === undefined) {\n continue;\n }\n const newKey = keyPrefix ? `${keyPrefix}${key}` : key;\n if (typeof value === 'object') {\n if (value instanceof Error) {\n // `message`/`stack` are non-enumerable, so the flatten path below would silently\n // reduce a logged error to nothing.\n result[newKey] = errorToString(value);\n } else if (!value || Array.isArray(value) || depth > FLATTEN_DEPTH) {\n result[newKey] = safeStringify(value);\n } else {\n const flattened = stringifyValues(value, `${newKey}_`, depth + 1);\n for (const [flattenedKey, flattenedValue] of Object.entries(flattened)) {\n result[flattenedKey] = flattenedValue;\n }\n }\n } else {\n result[newKey] = String(value);\n }\n }\n return result;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"chunk-logs.mjs","names":["otelContext"],"sources":["../../src/extensions/otel/logs.ts"],"sourcesContent":["//\n// Copyright 2024 DXOS.org\n//\n\nimport { type Context, context as otelContext, trace } from '@opentelemetry/api';\nimport { type AnyValueMap, SeverityNumber } from '@opentelemetry/api-logs';\nimport { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';\nimport { BatchLogRecordProcessor, LoggerProvider, type LogRecordExporter } from '@opentelemetry/sdk-logs';\nimport { ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';\n\nimport {\n type LogConfig,\n type LogEntry,\n LogLevel,\n type LogProcessor,\n getContextFromEntry,\n getRelativeFilename,\n} from '@dxos/log';\n\nimport { type OtelOptions, setDiagLogger, signalUrl } from './otel.ts';\n\nconst FLATTEN_DEPTH = 1;\n\nexport type OtelLogOptions = OtelOptions & {\n logLevel: LogLevel;\n exporter?: LogRecordExporter;\n /**\n * Batch processor sizing. The defaults (queue 2048, batches of 512) suit a live stream; a\n * one-shot dump of a whole log buffer must raise them or the processor drops records.\n */\n batch?: { maxQueueSize?: number; maxExportBatchSize?: number };\n /**\n * Called with the trace id of every entry at warning or above emitted inside a span, before the\n * export level is applied, so the tail sampler can keep that trace.\n */\n onTraceFlagged?: (traceId: string) => void;\n /**\n * Include logs forwarded from the shared worker via LoggingService.\n *\n * @deprecated\n */\n includeSharedWorkerLogs?: boolean;\n};\n\nexport class OtelLogs {\n private _loggerProvider: LoggerProvider;\n constructor(private readonly options: OtelLogOptions) {\n setDiagLogger(options.consoleDiagLogLevel);\n this._loggerProvider = new LoggerProvider({\n resource: this.options.resource,\n processors: this.options.destinations.map(\n (destination) =>\n new BatchLogRecordProcessor({\n exporter:\n options.exporter ??\n new OTLPLogExporter({\n url: signalUrl(destination, 'logs'),\n headers: destination.headers,\n concurrencyLimit: 10,\n }),\n ...options.batch,\n }),\n ),\n });\n }\n\n public readonly logProcessor: LogProcessor = (_config: LogConfig, entry: LogEntry) => {\n if (entry.level >= LogLevel.WARN && this.options.onTraceFlagged) {\n const traceId = trace.getSpan(otelContext.active())?.spanContext().traceId;\n if (traceId !== undefined) {\n this.options.onTraceFlagged(traceId);\n }\n }\n if (\n entry.level < this.options.logLevel ||\n (!this.options.includeSharedWorkerLogs && entry.meta?.S?.remoteSessionId)\n ) {\n return;\n }\n\n this.emit({\n severityNumber: convertLevel(entry.level),\n body: entry.message,\n timestamp: new Date(entry.timestamp),\n attributes: {\n ...(entry.meta ? { meta: { file: getRelativeFilename(entry.meta.F), line: entry.meta.L } } : {}),\n ...(entry.error ? { error: entry.error.stack } : {}),\n ...stringifyValues(getContextFromEntry(entry), 'ctx_'),\n },\n context: otelContext.active(),\n });\n };\n\n emit(record: {\n severityNumber: SeverityNumber;\n body?: string;\n timestamp: Date;\n attributes: AnyValueMap;\n /** Context whose span the record links to; absent for a record with no trace. */\n context?: Context;\n }): void {\n const logger = this._loggerProvider.getLogger(\n 'dxos-observability',\n this.options.resource.attributes[ATTR_SERVICE_VERSION]?.toString(),\n );\n\n logger.emit({\n severityNumber: record.severityNumber,\n body: record.body,\n timestamp: record.timestamp,\n attributes: { ...this.options.getTags(), ...record.attributes },\n context: record.context,\n });\n }\n\n flush(): Promise<void> {\n return this._loggerProvider.forceFlush();\n }\n\n close(): Promise<void> {\n return this._loggerProvider.shutdown();\n }\n}\n\n/** Maps {@link LogLevel} to OpenTelemetry {@link SeverityNumber}. */\nexport const convertLevel = (level: LogLevel): SeverityNumber => {\n switch (level) {\n case LogLevel.TRACE:\n return SeverityNumber.TRACE;\n case LogLevel.DEBUG:\n return SeverityNumber.DEBUG;\n case LogLevel.VERBOSE:\n return SeverityNumber.INFO;\n case LogLevel.INFO:\n return SeverityNumber.INFO;\n case LogLevel.WARN:\n return SeverityNumber.WARN;\n case LogLevel.ERROR:\n return SeverityNumber.ERROR;\n default:\n return SeverityNumber.UNSPECIFIED;\n }\n};\n\nconst errorToString = (error: Error): string => error.stack ?? `${error.name}: ${error.message}`;\n\nconst safeStringify = (value: unknown): string => {\n try {\n // Replace errors as they are encountered; `JSON.stringify` alone renders them as `{}`.\n return JSON.stringify(value, (_key, nested) => (nested instanceof Error ? errorToString(nested) : nested));\n } catch {\n return '[Circular]';\n }\n};\n\n// TODO(wittjosiah): Reconcile logging utils w/ EDGE.\nexport const stringifyValues = (object: object | undefined, keyPrefix?: string, depth: number = 1) => {\n if (!object) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(object)) {\n if (value === undefined) {\n continue;\n }\n const newKey = keyPrefix ? `${keyPrefix}${key}` : key;\n if (typeof value === 'object') {\n if (value instanceof Error) {\n // `message`/`stack` are non-enumerable, so the flatten path below would silently\n // reduce a logged error to nothing.\n result[newKey] = errorToString(value);\n } else if (!value || Array.isArray(value) || depth > FLATTEN_DEPTH) {\n result[newKey] = safeStringify(value);\n } else {\n const flattened = stringifyValues(value, `${newKey}_`, depth + 1);\n for (const [flattenedKey, flattenedValue] of Object.entries(flattened)) {\n result[flattenedKey] = flattenedValue;\n }\n }\n } else {\n result[newKey] = String(value);\n }\n }\n return result;\n};\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { Sink } from "./extensions/otel/OtelLogSink.mjs";
1
+ import { n as Sink } from "./chunk-OtelLogSink.mjs";
2
2
  import { LogLevel } from "@dxos/log";
3
3
  //#region src/extensions/posthog/support-logs.ts
4
4
  var FLUSH_BATCH_SIZE = 1e3;
@@ -1 +1 @@
1
- {"version":3,"file":"chunk-support-logs.mjs","names":[],"sources":["../../src/extensions/posthog/support-logs.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport { LogLevel } from '@dxos/log';\n\nimport { type OtelDestination } from '../otel/otel.ts';\nimport * as OtelLogSink from '../otel/OtelLogSink.ts';\n\nconst FLUSH_BATCH_SIZE = 1_000;\n\nexport type FlushSupportLogsOptions = {\n destination: OtelDestination;\n resourceAttributes: Record<string, string>;\n attributes: Record<string, string>;\n};\n\n/** Ships a JSONL log dump to PostHog Logs as one OTLP stream, returning the number of lines sent. */\nexport const flushSupportLogs = async (ndjson: string, options: FlushSupportLogsOptions): Promise<number> => {\n const sink = new OtelLogSink.Sink(\n {\n type: 'otel-init',\n destinations: [options.destination],\n resourceAttributes: options.resourceAttributes,\n logLevel: LogLevel.TRACE,\n tags: options.attributes,\n },\n { batch: { maxQueueSize: FLUSH_BATCH_SIZE * 2, maxExportBatchSize: FLUSH_BATCH_SIZE } },\n );\n\n let count = 0;\n try {\n for (const line of ndjson.split('\\n')) {\n if (line.length === 0) {\n continue;\n }\n sink.append(line);\n count++;\n if (count % FLUSH_BATCH_SIZE === 0) {\n await sink.flush();\n }\n }\n await sink.flush();\n } finally {\n await sink.close();\n }\n return count;\n};\n"],"mappings":";;;AASA,IAAM,mBAAmB;;AASzB,IAAa,mBAAmB,OAAO,QAAgB,YAAsD;CAC3G,MAAM,OAAO,IAAI,KACf;EACE,MAAM;EACN,cAAc,CAAC,QAAQ,WAAW;EAClC,oBAAoB,QAAQ;EAC5B,UAAU,SAAS;EACnB,MAAM,QAAQ;CAChB,GACA,EAAE,OAAO;EAAE,cAAc,mBAAmB;EAAG,oBAAoB;CAAiB,EAAE,CACxF;CAEA,IAAI,QAAQ;CACZ,IAAI;EACF,KAAK,MAAM,QAAQ,OAAO,MAAM,IAAI,GAAG;GACrC,IAAI,KAAK,WAAW,GAClB;GAEF,KAAK,OAAO,IAAI;GAChB;GACA,IAAI,QAAQ,qBAAqB,GAC/B,MAAM,KAAK,MAAM;EAErB;EACA,MAAM,KAAK,MAAM;CACnB,UAAU;EACR,MAAM,KAAK,MAAM;CACnB;CACA,OAAO;AACT"}
1
+ {"version":3,"file":"chunk-support-logs.mjs","names":["OtelLogSink.Sink"],"sources":["../../src/extensions/posthog/support-logs.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport { LogLevel } from '@dxos/log';\n\nimport { type OtelDestination } from '../otel/otel.ts';\nimport * as OtelLogSink from '../otel/OtelLogSink.ts';\n\nconst FLUSH_BATCH_SIZE = 1_000;\n\nexport type FlushSupportLogsOptions = {\n destination: OtelDestination;\n resourceAttributes: Record<string, string>;\n attributes: Record<string, string>;\n};\n\n/** Ships a JSONL log dump to PostHog Logs as one OTLP stream, returning the number of lines sent. */\nexport const flushSupportLogs = async (ndjson: string, options: FlushSupportLogsOptions): Promise<number> => {\n const sink = new OtelLogSink.Sink(\n {\n type: 'otel-init',\n destinations: [options.destination],\n resourceAttributes: options.resourceAttributes,\n logLevel: LogLevel.TRACE,\n tags: options.attributes,\n },\n { batch: { maxQueueSize: FLUSH_BATCH_SIZE * 2, maxExportBatchSize: FLUSH_BATCH_SIZE } },\n );\n\n let count = 0;\n try {\n for (const line of ndjson.split('\\n')) {\n if (line.length === 0) {\n continue;\n }\n sink.append(line);\n count++;\n if (count % FLUSH_BATCH_SIZE === 0) {\n await sink.flush();\n }\n }\n await sink.flush();\n } finally {\n await sink.close();\n }\n return count;\n};\n"],"mappings":";;;AASA,IAAM,mBAAmB;;AASzB,IAAa,mBAAmB,OAAO,QAAgB,YAAsD;CAC3G,MAAM,OAAO,IAAIA,KACf;EACE,MAAM;EACN,cAAc,CAAC,QAAQ,WAAW;EAClC,oBAAoB,QAAQ;EAC5B,UAAU,SAAS;EACnB,MAAM,QAAQ;CAChB,GACA,EAAE,OAAO;EAAE,cAAc,mBAAmB;EAAG,oBAAoB;CAAiB,EAAE,CACxF;CAEA,IAAI,QAAQ;CACZ,IAAI;EACF,KAAK,MAAM,QAAQ,OAAO,MAAM,IAAI,GAAG;GACrC,IAAI,KAAK,WAAW,GAClB;GAEF,KAAK,OAAO,IAAI;GAChB;GACA,IAAI,QAAQ,qBAAqB,GAC/B,MAAM,KAAK,MAAM;EAErB;EACA,MAAM,KAAK,MAAM;CACnB,UAAU;EACR,MAAM,KAAK,MAAM;CACnB;CACA,OAAO;AACT"}
@@ -1,105 +1 @@
1
- import { t as __exportAll } from "../../chunk-rolldown-runtime.mjs";
2
- import { n as contextForTrace } from "../../chunk-trace-context.mjs";
3
- import { n as convertLevel, t as OtelLogs } from "../../chunk-logs.mjs";
4
- import { LogLevel, log, shortLevelName } from "@dxos/log";
5
- import { defaultResource, resourceFromAttributes } from "@opentelemetry/resources";
6
- //#region src/extensions/otel/OtelLogSink.ts
7
- var OtelLogSink_exports = /* @__PURE__ */ __exportAll({ Sink: () => Sink });
8
- var __dxlog_file = "/__w/dxos/dxos/packages/sdk/observability/src/extensions/otel/OtelLogSink.ts";
9
- var levelFromShortName = new Map([
10
- LogLevel.TRACE,
11
- LogLevel.DEBUG,
12
- LogLevel.VERBOSE,
13
- LogLevel.INFO,
14
- LogLevel.WARN,
15
- LogLevel.ERROR
16
- ].map((level) => [shortLevelName[level], level]));
17
- var Sink = class {
18
- #logs;
19
- #tags;
20
- #logLevel;
21
- #onTraceFlagged;
22
- constructor(init, options = {}) {
23
- this.#tags = { ...init.tags };
24
- this.#logLevel = init.logLevel;
25
- this.#onTraceFlagged = options.onTraceFlagged;
26
- this.#logs = new OtelLogs({
27
- destinations: init.destinations,
28
- resource: createResource(init.resourceAttributes),
29
- getTags: () => this.#tags,
30
- logLevel: init.logLevel,
31
- exporter: options.exporter,
32
- batch: options.batch
33
- });
34
- }
35
- /**
36
- * Ingest one pre-serialized JSONL log line (the same string the IDB store appends).
37
- * Never throws — a malformed line is dropped.
38
- */
39
- append(line) {
40
- let record;
41
- try {
42
- record = JSON.parse(line);
43
- } catch {
44
- return;
45
- }
46
- if (record === null || typeof record !== "object") return;
47
- const level = levelFromShortName.get(record.l);
48
- if (level === void 0) return;
49
- if (level >= LogLevel.WARN && record.r !== void 0) this.#onTraceFlagged?.(record.r);
50
- if (level < this.#logLevel) return;
51
- this.#logs.emit({
52
- severityNumber: convertLevel(level),
53
- body: record.m,
54
- timestamp: new Date(record.t),
55
- attributes: {
56
- ...record.f !== void 0 ? { meta: {
57
- file: record.f,
58
- line: record.n
59
- } } : {},
60
- ...record.e !== void 0 ? { error: record.e } : {},
61
- ...parseContext(record.c)
62
- },
63
- context: record.r !== void 0 && record.s !== void 0 ? contextForTrace({
64
- traceId: record.r,
65
- spanId: record.s
66
- }) : void 0
67
- });
68
- }
69
- handleMessage(message) {
70
- switch (message.type) {
71
- case "otel-tags":
72
- this.#tags = {
73
- ...this.#tags,
74
- ...message.tags
75
- };
76
- break;
77
- case "otel-flush": this.flush().catch((err) => log.catch(err, void 0, {
78
- "~LogMeta": "~LogMeta",
79
- F: __dxlog_file,
80
- L: 115,
81
- S: this
82
- }));
83
- }
84
- }
85
- flush() {
86
- return this.#logs.flush();
87
- }
88
- close() {
89
- return this.#logs.close();
90
- }
91
- };
92
- var createResource = (attributes) => defaultResource().merge(resourceFromAttributes(attributes));
93
- var parseContext = (context) => {
94
- if (!context) return {};
95
- try {
96
- const parsed = JSON.parse(context);
97
- return Object.fromEntries(Object.entries(parsed).filter(([, value]) => value !== void 0).map(([key, value]) => [`ctx_${key}`, typeof value === "string" ? value : JSON.stringify(value)]));
98
- } catch {
99
- return {};
100
- }
101
- };
102
- //#endregion
103
- export { Sink, OtelLogSink_exports as t };
104
-
105
- //# sourceMappingURL=OtelLogSink.mjs.map
1
+ export { n as Sink } from "../../chunk-OtelLogSink.mjs";