@obsunified/collector 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/auth.d.ts +2 -0
  4. package/dist/auth.js +9 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/chunk-UGWDOGGH.js +319 -0
  7. package/dist/chunk-UGWDOGGH.js.map +1 -0
  8. package/dist/chunk-XKQTVIRE.js +671 -0
  9. package/dist/chunk-XKQTVIRE.js.map +1 -0
  10. package/dist/index.d.ts +1197 -0
  11. package/dist/index.js +17707 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/ingest-auth-HFVO1zIp.d.ts +165 -0
  14. package/dist/openai-FJYOQ7WO.js +384 -0
  15. package/dist/openai-FJYOQ7WO.js.map +1 -0
  16. package/package.json +73 -0
  17. package/src/ai-parity/ai-parity.acceptance.test.ts +449 -0
  18. package/src/analyses/derive.ts +642 -0
  19. package/src/analyses/index.ts +50 -0
  20. package/src/analyses/investigations.ts +277 -0
  21. package/src/analyses/tier0.ts +568 -0
  22. package/src/auth/dashboard-auth.ts +269 -0
  23. package/src/auth/ingest-auth.ts +147 -0
  24. package/src/durable-objects/tail-hub.test.ts +32 -0
  25. package/src/durable-objects/tail-hub.ts +158 -0
  26. package/src/framework/collector.ts +467 -0
  27. package/src/framework/env.ts +90 -0
  28. package/src/framework/logger.ts +81 -0
  29. package/src/index.ts +211 -0
  30. package/src/lib/action-aggregates.ts +1031 -0
  31. package/src/lib/action-identity.ts +106 -0
  32. package/src/lib/actions-store.ts +397 -0
  33. package/src/lib/ai-pricing.ts +80 -0
  34. package/src/lib/ai-store/calls.ts +148 -0
  35. package/src/lib/ai-store/evaluations.test.ts +47 -0
  36. package/src/lib/ai-store/evaluations.ts +125 -0
  37. package/src/lib/ai-store/helpers.ts +37 -0
  38. package/src/lib/ai-store/retention.ts +17 -0
  39. package/src/lib/ai-store/sessions.ts +277 -0
  40. package/src/lib/ai-store/spans.ts +109 -0
  41. package/src/lib/ai-store/types.ts +112 -0
  42. package/src/lib/ai-store.ts +76 -0
  43. package/src/lib/alerts-store.ts +458 -0
  44. package/src/lib/analyses-runner.ts +439 -0
  45. package/src/lib/analyses-store.test.ts +243 -0
  46. package/src/lib/analyses-store.ts +454 -0
  47. package/src/lib/ask.test.ts +302 -0
  48. package/src/lib/ask.ts +390 -0
  49. package/src/lib/blob-store-s3.ts +213 -0
  50. package/src/lib/blob-store.ts +366 -0
  51. package/src/lib/eval-cases-store.test.ts +171 -0
  52. package/src/lib/eval-cases-store.ts +1033 -0
  53. package/src/lib/evidence-references.test.ts +10 -0
  54. package/src/lib/evidence-references.ts +684 -0
  55. package/src/lib/hash.ts +26 -0
  56. package/src/lib/identity-index/action-lookups.ts +805 -0
  57. package/src/lib/identity-index/constants.ts +1 -0
  58. package/src/lib/identity-index/key-lookups.ts +199 -0
  59. package/src/lib/identity-index/mappers.ts +349 -0
  60. package/src/lib/identity-index/types.ts +196 -0
  61. package/src/lib/identity-index/user-lookup.ts +137 -0
  62. package/src/lib/identity-index.test.ts +624 -0
  63. package/src/lib/identity-index.ts +85 -0
  64. package/src/lib/json.ts +39 -0
  65. package/src/lib/llm.ts +263 -0
  66. package/src/lib/logs-store.ts +188 -0
  67. package/src/lib/metrics-store.test.ts +189 -0
  68. package/src/lib/metrics-store.ts +312 -0
  69. package/src/lib/narrate-gate.test.ts +260 -0
  70. package/src/lib/narrate-gate.ts +253 -0
  71. package/src/lib/openai.test.ts +300 -0
  72. package/src/lib/openai.ts +512 -0
  73. package/src/lib/otlp.ts +212 -0
  74. package/src/lib/parse-pprof.test.ts +64 -0
  75. package/src/lib/parse-pprof.ts +184 -0
  76. package/src/lib/payload-capture.ts +22 -0
  77. package/src/lib/projects-store.ts +207 -0
  78. package/src/lib/propagation-metric.test.ts +196 -0
  79. package/src/lib/propagation-metric.ts +175 -0
  80. package/src/lib/sql-db-dialect.test.ts +22 -0
  81. package/src/lib/sql-db-postgres.ts +229 -0
  82. package/src/lib/sql-db.ts +166 -0
  83. package/src/lib/stage6.test.ts +216 -0
  84. package/src/lib/store/export.ts +65 -0
  85. package/src/lib/store/helpers.ts +367 -0
  86. package/src/lib/store/ingest.ts +68 -0
  87. package/src/lib/store/issues.ts +158 -0
  88. package/src/lib/store/overview.ts +127 -0
  89. package/src/lib/store/retention.ts +10 -0
  90. package/src/lib/store/service-map.ts +384 -0
  91. package/src/lib/store/trace-detail.bench.ts +76 -0
  92. package/src/lib/store/trace-detail.test.ts +410 -0
  93. package/src/lib/store/trace-detail.ts +427 -0
  94. package/src/lib/store/trace-queries.test.ts +29 -0
  95. package/src/lib/store/trace-queries.ts +89 -0
  96. package/src/lib/store-overview.test.ts +77 -0
  97. package/src/lib/store.ts +96 -0
  98. package/src/lib/tail-publisher.ts +64 -0
  99. package/src/lib/test-utils/mem-sql-db.test.ts +93 -0
  100. package/src/lib/test-utils/mem-sql-db.ts +86 -0
  101. package/src/lib/ua-parser.ts +61 -0
  102. package/src/lib/usage-store.ts +567 -0
  103. package/src/migrations/001_telemetry_spans.sql +28 -0
  104. package/src/migrations/002_usage_events.sql +24 -0
  105. package/src/migrations/003_usage_analytics_columns.sql +13 -0
  106. package/src/migrations/004_logs.sql +19 -0
  107. package/src/migrations/005_ai_calls.sql +25 -0
  108. package/src/migrations/006_user_profiles.sql +11 -0
  109. package/src/migrations/007_session_replay_metadata.sql +10 -0
  110. package/src/migrations/008_session_replay_storage_bytes.sql +1 -0
  111. package/src/migrations/009_projects_and_keys.sql +22 -0
  112. package/src/migrations/010_signal_project_id.sql +15 -0
  113. package/src/migrations/011_default_project_seed.sql +3 -0
  114. package/src/migrations/012_alert_rules.sql +17 -0
  115. package/src/migrations/013_alert_evaluations.sql +13 -0
  116. package/src/migrations/014_alert_state.sql +9 -0
  117. package/src/migrations/015_metrics.sql +58 -0
  118. package/src/migrations/016_metrics_exp_histogram_summary.sql +5 -0
  119. package/src/migrations/017_span_dropped_counts.sql +11 -0
  120. package/src/migrations/018_log_flags.sql +8 -0
  121. package/src/migrations/019_ai_span_payloads.sql +30 -0
  122. package/src/migrations/020_ai_span_evaluations.sql +33 -0
  123. package/src/migrations/021_ai_span_session.sql +13 -0
  124. package/src/migrations/022_span_log_session_id.sql +13 -0
  125. package/src/migrations/023_service_map_indexes.sql +32 -0
  126. package/src/migrations/024_analysis_results.sql +49 -0
  127. package/src/migrations/025_stage6_autopinning_alerts.sql +38 -0
  128. package/src/migrations/026_obs_dashboard_project_seed.sql +18 -0
  129. package/src/migrations/027_identity_propagation.sql +50 -0
  130. package/src/migrations/028_profile_blobs.sql +57 -0
  131. package/src/migrations/029_telemetry_sdk_name.sql +20 -0
  132. package/src/migrations/030_analysis_claim.sql +18 -0
  133. package/src/migrations/031_agent_action_graph.sql +154 -0
  134. package/src/migrations/032_payload_capture_flags.sql +8 -0
  135. package/src/migrations/033_eval_cases.sql +52 -0
  136. package/src/migrations/034_eval_case_results.sql +23 -0
  137. package/src/migrations/035_metric_exemplars.sql +33 -0
  138. package/src/migrations/036_trace_instrumentation_gaps.sql +19 -0
  139. package/src/migrations/037_eval_runs.sql +37 -0
  140. package/src/migrations/038_tool_call_evidence.sql +11 -0
  141. package/src/migrations/039_drop_trace_instrumentation_gaps.sql +11 -0
  142. package/src/migrations/040_evidence_retrieval_refs.sql +50 -0
  143. package/src/migrations-postgres/001_telemetry_spans.sql +33 -0
  144. package/src/migrations-postgres/002_usage_events.sql +24 -0
  145. package/src/migrations-postgres/003_usage_analytics_columns.sql +13 -0
  146. package/src/migrations-postgres/004_logs.sql +19 -0
  147. package/src/migrations-postgres/005_ai_calls.sql +25 -0
  148. package/src/migrations-postgres/006_user_profiles.sql +11 -0
  149. package/src/migrations-postgres/007_session_replay_metadata.sql +10 -0
  150. package/src/migrations-postgres/008_session_replay_storage_bytes.sql +2 -0
  151. package/src/migrations-postgres/009_projects_and_keys.sql +25 -0
  152. package/src/migrations-postgres/010_signal_project_id.sql +18 -0
  153. package/src/migrations-postgres/011_default_project_seed.sql +5 -0
  154. package/src/migrations-postgres/012_alert_rules.sql +17 -0
  155. package/src/migrations-postgres/013_alert_evaluations.sql +13 -0
  156. package/src/migrations-postgres/014_alert_state.sql +9 -0
  157. package/src/migrations-postgres/015_metrics.sql +47 -0
  158. package/src/migrations-postgres/016_metrics_exp_histogram_summary.sql +1 -0
  159. package/src/migrations-postgres/017_span_dropped_counts.sql +4 -0
  160. package/src/migrations-postgres/018_log_flags.sql +2 -0
  161. package/src/migrations-postgres/019_ai_span_payloads.sql +18 -0
  162. package/src/migrations-postgres/020_ai_span_evaluations.sql +21 -0
  163. package/src/migrations-postgres/021_ai_span_session.sql +7 -0
  164. package/src/migrations-postgres/022_span_log_session_id.sql +7 -0
  165. package/src/migrations-postgres/023_service_map_indexes.sql +9 -0
  166. package/src/migrations-postgres/024_analysis_results.sql +41 -0
  167. package/src/migrations-postgres/025_stage6_autopinning_alerts.sql +17 -0
  168. package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +14 -0
  169. package/src/migrations-postgres/027_identity_propagation.sql +27 -0
  170. package/src/migrations-postgres/028_profile_blobs.sql +48 -0
  171. package/src/migrations-postgres/029_telemetry_sdk_name.sql +5 -0
  172. package/src/migrations-postgres/030_analysis_claim.sql +5 -0
  173. package/src/migrations-postgres/031_agent_action_graph.sql +190 -0
  174. package/src/migrations-postgres/032_payload_capture_flags.sql +8 -0
  175. package/src/migrations-postgres/033_eval_cases.sql +55 -0
  176. package/src/migrations-postgres/034_eval_case_results.sql +26 -0
  177. package/src/migrations-postgres/035_metric_exemplars.sql +31 -0
  178. package/src/migrations-postgres/036_trace_instrumentation_gaps.sql +19 -0
  179. package/src/migrations-postgres/037_eval_runs.sql +40 -0
  180. package/src/migrations-postgres/038_tool_call_evidence.sql +11 -0
  181. package/src/migrations-postgres/039_drop_trace_instrumentation_gaps.sql +11 -0
  182. package/src/migrations-postgres/040_evidence_retrieval_refs.sql +47 -0
  183. package/src/migrations-postgres/README.md +40 -0
  184. package/src/otlp/acceptance.test.ts +373 -0
  185. package/src/otlp/decode/body.ts +127 -0
  186. package/src/otlp/decode/logs.ts +96 -0
  187. package/src/otlp/decode/metrics.ts +402 -0
  188. package/src/otlp/decode/traces.ts +92 -0
  189. package/src/otlp/decode/values.ts +150 -0
  190. package/src/otlp/decode.test.ts +800 -0
  191. package/src/otlp/decode.ts +11 -0
  192. package/src/otlp/gen/opentelemetry/proto/collector/logs/v1/logs_service_pb.ts +132 -0
  193. package/src/otlp/gen/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.ts +158 -0
  194. package/src/otlp/gen/opentelemetry/proto/collector/trace/v1/trace_service_pb.ts +158 -0
  195. package/src/otlp/gen/opentelemetry/proto/common/v1/common_pb.ts +289 -0
  196. package/src/otlp/gen/opentelemetry/proto/logs/v1/logs_pb.ts +397 -0
  197. package/src/otlp/gen/opentelemetry/proto/metrics/v1/metrics_pb.ts +1329 -0
  198. package/src/otlp/gen/opentelemetry/proto/resource/v1/resource_pb.ts +86 -0
  199. package/src/otlp/gen/opentelemetry/proto/trace/v1/trace_pb.ts +710 -0
  200. package/src/otlp/response.test.ts +158 -0
  201. package/src/otlp/response.ts +129 -0
  202. package/src/plugins/_context.ts +15 -0
  203. package/src/plugins/action-graph-processor/enrichers.ts +45 -0
  204. package/src/plugins/action-graph-processor/redaction.ts +84 -0
  205. package/src/plugins/action-graph-processor.test.ts +1023 -0
  206. package/src/plugins/action-graph-processor.ts +1172 -0
  207. package/src/plugins/action-routes.test.ts +716 -0
  208. package/src/plugins/action-routes.ts +343 -0
  209. package/src/plugins/ai-receiver.ts +254 -0
  210. package/src/plugins/ai-span-payloads-processor.test.ts +90 -0
  211. package/src/plugins/ai-span-payloads-processor.ts +180 -0
  212. package/src/plugins/alerts-evaluator.test.ts +91 -0
  213. package/src/plugins/alerts-evaluator.ts +328 -0
  214. package/src/plugins/alerts-routes.ts +101 -0
  215. package/src/plugins/analyses-routes.ts +138 -0
  216. package/src/plugins/ask-routes.ts +182 -0
  217. package/src/plugins/bot-filter.ts +19 -0
  218. package/src/plugins/connected-routes/manifest.ts +531 -0
  219. package/src/plugins/connected-routes.test.ts +1791 -0
  220. package/src/plugins/connected-routes.ts +1103 -0
  221. package/src/plugins/dashboard-routes.ts +41 -0
  222. package/src/plugins/default-span-enrichment.ts +92 -0
  223. package/src/plugins/eval-cases-routes.test.ts +391 -0
  224. package/src/plugins/eval-cases-routes.ts +529 -0
  225. package/src/plugins/evidence-retrieval-routes.benchmark.test.ts +245 -0
  226. package/src/plugins/evidence-retrieval-routes.test.ts +932 -0
  227. package/src/plugins/evidence-retrieval-routes.ts +2179 -0
  228. package/src/plugins/gen-ai-normalizer.test.ts +680 -0
  229. package/src/plugins/gen-ai-normalizer.ts +302 -0
  230. package/src/plugins/identity-receiver.ts +96 -0
  231. package/src/plugins/issue-enrichment.ts +132 -0
  232. package/src/plugins/issue-insights.ts +81 -0
  233. package/src/plugins/logs-receiver.ts +160 -0
  234. package/src/plugins/metrics-receiver.ts +91 -0
  235. package/src/plugins/onboarding-routes.ts +54 -0
  236. package/src/plugins/otlp-receiver.ts +124 -0
  237. package/src/plugins/platform-routes.ts +165 -0
  238. package/src/plugins/profile-routes.ts +468 -0
  239. package/src/plugins/projects-routes.ts +92 -0
  240. package/src/plugins/query-routes.ts +220 -0
  241. package/src/plugins/redaction-processor.ts +132 -0
  242. package/src/plugins/replay-query-routes.test.ts +38 -0
  243. package/src/plugins/replay-query-routes.ts +199 -0
  244. package/src/plugins/replay-receiver.ts +81 -0
  245. package/src/plugins/tail-routes.test.ts +47 -0
  246. package/src/plugins/tail-routes.ts +42 -0
  247. package/src/plugins/timeline-routes.ts +244 -0
  248. package/src/plugins/ua-enrichment.ts +27 -0
  249. package/src/plugins/usage-normalization.ts +46 -0
  250. package/src/plugins/usage-privacy.ts +55 -0
  251. package/src/plugins/usage-query-routes.ts +102 -0
  252. package/src/plugins/usage-receiver.ts +129 -0
  253. package/src/plugins/users-query-routes.ts +78 -0
  254. package/src/plugins/utm-enrichment.ts +36 -0
@@ -0,0 +1,96 @@
1
+ import { fromBinary, fromJson } from "@bufbuild/protobuf";
2
+ import type { LogSeverity, JsonValue as ObsJsonValue } from "@obsunified/types";
3
+ import {
4
+ type ExportLogsServiceRequest,
5
+ ExportLogsServiceRequestSchema,
6
+ } from "../gen/opentelemetry/proto/collector/logs/v1/logs_service_pb.js";
7
+ import type { LogRecord as OtlpLogRecord } from "../gen/opentelemetry/proto/logs/v1/logs_pb.js";
8
+ import { decodeJsonBody, OtlpDecodeError, type ReadBodyResult } from "./body";
9
+ import {
10
+ anyValueToString,
11
+ bytesToHex,
12
+ extractServiceName,
13
+ keyValuesToRecord,
14
+ severityFromNumber,
15
+ } from "./values";
16
+
17
+ // ── Logs ─────────────────────────────────────────────────────────────
18
+
19
+ /**
20
+ * A log record normalized for storage. One per emitted OTLP `LogRecord`,
21
+ * with resource / scope context flattened into the fields the `LogsStore`
22
+ * expects. Receiver adds storage-level fields (projectId, logId, receivedAt,
23
+ * expiresAt) at ingest time.
24
+ */
25
+ export interface DecodedLogRecord {
26
+ serviceName: string | null;
27
+ loggerName: string | null;
28
+ traceId: string | null;
29
+ spanId: string | null;
30
+ severity: LogSeverity;
31
+ severityNumber: number;
32
+ message: string;
33
+ attributes: Record<string, ObsJsonValue> | null;
34
+ flags: number;
35
+ droppedAttributesCount: number;
36
+ occurredAt: string;
37
+ }
38
+
39
+ /**
40
+ * Decode an OTLP logs export request into a flat list of records ready for
41
+ * storage. Resource `service.name` and scope name are denormalized onto each
42
+ * record so downstream code doesn't need to traverse the hierarchy.
43
+ */
44
+ export const decodeLogsRequest = (body: ReadBodyResult): DecodedLogRecord[] => {
45
+ let msg: ExportLogsServiceRequest;
46
+ try {
47
+ msg =
48
+ body.wireFormat === "protobuf"
49
+ ? fromBinary(ExportLogsServiceRequestSchema, body.bytes)
50
+ : fromJson(ExportLogsServiceRequestSchema, decodeJsonBody(body.bytes));
51
+ } catch (err) {
52
+ throw new OtlpDecodeError(
53
+ `Malformed OTLP body: ${(err as Error).message}`,
54
+ 400,
55
+ );
56
+ }
57
+
58
+ const out: DecodedLogRecord[] = [];
59
+ for (const rl of msg.resourceLogs) {
60
+ const serviceName = extractServiceName(rl.resource?.attributes);
61
+ for (const sl of rl.scopeLogs) {
62
+ const loggerName = sl.scope?.name || null;
63
+ for (const log of sl.logRecords) {
64
+ out.push(adaptLogRecord(log, serviceName, loggerName));
65
+ }
66
+ }
67
+ }
68
+ return out;
69
+ };
70
+
71
+ const adaptLogRecord = (
72
+ log: OtlpLogRecord,
73
+ serviceName: string | null,
74
+ loggerName: string | null,
75
+ ): DecodedLogRecord => {
76
+ const ts = log.timeUnixNano || log.observedTimeUnixNano;
77
+ const occurredAt =
78
+ ts > 0n
79
+ ? new Date(Number(ts / 1_000_000n)).toISOString()
80
+ : new Date().toISOString();
81
+ return {
82
+ serviceName,
83
+ loggerName,
84
+ traceId: log.traceId.length === 16 ? bytesToHex(log.traceId) : null,
85
+ spanId: log.spanId.length === 8 ? bytesToHex(log.spanId) : null,
86
+ severity: severityFromNumber(log.severityNumber, log.severityText),
87
+ severityNumber: log.severityNumber || 0,
88
+ message: anyValueToString(log.body),
89
+ attributes: log.attributes.length
90
+ ? keyValuesToRecord(log.attributes)
91
+ : null,
92
+ flags: log.flags || 0,
93
+ droppedAttributesCount: log.droppedAttributesCount || 0,
94
+ occurredAt,
95
+ };
96
+ };
@@ -0,0 +1,402 @@
1
+ import { fromBinary, fromJson } from "@bufbuild/protobuf";
2
+ import {
3
+ type ExportMetricsServiceRequest,
4
+ ExportMetricsServiceRequestSchema,
5
+ } from "../gen/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.js";
6
+ import type { KeyValue } from "../gen/opentelemetry/proto/common/v1/common_pb.js";
7
+ import type {
8
+ Exemplar,
9
+ ExponentialHistogramDataPoint,
10
+ HistogramDataPoint,
11
+ Metric,
12
+ NumberDataPoint,
13
+ ResourceMetrics,
14
+ ScopeMetrics,
15
+ SummaryDataPoint,
16
+ } from "../gen/opentelemetry/proto/metrics/v1/metrics_pb.js";
17
+ import { decodeJsonBody, OtlpDecodeError, type ReadBodyResult } from "./body";
18
+ import { bytesToHex, extractServiceName, keyValuesToRecord } from "./values";
19
+
20
+ // ── Metrics ───────────────────────────────────────────────────────────
21
+
22
+ export type MetricType =
23
+ | "gauge"
24
+ | "sum"
25
+ | "histogram"
26
+ | "exp_histogram"
27
+ | "summary";
28
+
29
+ export interface DecodedExemplar {
30
+ value: number;
31
+ traceId: string | null;
32
+ spanId: string | null;
33
+ tsNs: string;
34
+ }
35
+
36
+ /**
37
+ * A metric data point normalized for storage. Series-identifying fields
38
+ * (name, resource/scope context, attributes) are repeated on each point so
39
+ * the store can do series upsert in a single pass.
40
+ */
41
+ export interface DecodedMetricPoint {
42
+ // Series identity
43
+ name: string;
44
+ description: string | null;
45
+ unit: string | null;
46
+ type: MetricType;
47
+ isMonotonic: boolean | null;
48
+ temporality: number | null;
49
+ scopeName: string | null;
50
+ scopeVersion: string | null;
51
+ serviceName: string | null;
52
+ resourceAttrsJson: string | null;
53
+ attributesJson: string | null;
54
+ identity: string;
55
+
56
+ // Point
57
+ tsNs: string;
58
+ startTsNs: string | null;
59
+ value: number | null;
60
+
61
+ // Histogram-only
62
+ count: number | null;
63
+ sum: number | null;
64
+ min: number | null;
65
+ max: number | null;
66
+ boundsJson: string | null;
67
+ bucketCountsJson: string | null;
68
+
69
+ /**
70
+ * Type-specific fields that don't fit the column schema:
71
+ * - exp_histogram: { scale, zeroCount, zeroThreshold, positive: {offset, bucketCounts}, negative: {offset, bucketCounts} }
72
+ * - summary: { quantileValues: [{quantile, value}, ...] }
73
+ */
74
+ extraJson: string | null;
75
+
76
+ exemplarsJson: string | null;
77
+ }
78
+
79
+ export const decodeMetricsRequest = (
80
+ body: ReadBodyResult,
81
+ ): DecodedMetricPoint[] => {
82
+ let msg: ExportMetricsServiceRequest;
83
+ try {
84
+ msg =
85
+ body.wireFormat === "protobuf"
86
+ ? fromBinary(ExportMetricsServiceRequestSchema, body.bytes)
87
+ : fromJson(
88
+ ExportMetricsServiceRequestSchema,
89
+ decodeJsonBody(body.bytes),
90
+ );
91
+ } catch (err) {
92
+ throw new OtlpDecodeError(
93
+ `Malformed OTLP body: ${(err as Error).message}`,
94
+ 400,
95
+ );
96
+ }
97
+
98
+ const out: DecodedMetricPoint[] = [];
99
+ for (const rm of msg.resourceMetrics) {
100
+ const resourceAttrs = rm.resource?.attributes ?? [];
101
+ const resourceAttrsJson = resourceAttrs.length
102
+ ? JSON.stringify(keyValuesToRecord(resourceAttrs))
103
+ : null;
104
+ const serviceName = extractServiceName(resourceAttrs);
105
+ for (const sm of rm.scopeMetrics) {
106
+ addMetrics(out, rm, sm, serviceName, resourceAttrsJson);
107
+ }
108
+ }
109
+ return out;
110
+ };
111
+
112
+ const addMetrics = (
113
+ out: DecodedMetricPoint[],
114
+ rm: ResourceMetrics,
115
+ sm: ScopeMetrics,
116
+ serviceName: string | null,
117
+ resourceAttrsJson: string | null,
118
+ ) => {
119
+ const scopeName = sm.scope?.name || null;
120
+ const scopeVersion = sm.scope?.version || null;
121
+ const resourceIdentityFragment = canonicalKeyValues(
122
+ rm.resource?.attributes ?? [],
123
+ );
124
+
125
+ for (const metric of sm.metrics) {
126
+ const common = {
127
+ name: metric.name,
128
+ description: metric.description || null,
129
+ unit: metric.unit || null,
130
+ scopeName,
131
+ scopeVersion,
132
+ serviceName,
133
+ resourceAttrsJson,
134
+ };
135
+ switch (metric.data.case) {
136
+ case "gauge":
137
+ for (const p of metric.data.value.dataPoints) {
138
+ out.push(
139
+ numberPoint(metric, p, "gauge", common, resourceIdentityFragment),
140
+ );
141
+ }
142
+ break;
143
+ case "sum":
144
+ for (const p of metric.data.value.dataPoints) {
145
+ out.push({
146
+ ...numberPoint(metric, p, "sum", common, resourceIdentityFragment),
147
+ isMonotonic: metric.data.value.isMonotonic,
148
+ temporality: metric.data.value.aggregationTemporality,
149
+ });
150
+ }
151
+ break;
152
+ case "histogram":
153
+ for (const p of metric.data.value.dataPoints) {
154
+ out.push({
155
+ ...histogramPoint(metric, p, common, resourceIdentityFragment),
156
+ temporality: metric.data.value.aggregationTemporality,
157
+ });
158
+ }
159
+ break;
160
+ case "exponentialHistogram":
161
+ for (const p of metric.data.value.dataPoints) {
162
+ out.push({
163
+ ...expHistogramPoint(metric, p, common, resourceIdentityFragment),
164
+ temporality: metric.data.value.aggregationTemporality,
165
+ });
166
+ }
167
+ break;
168
+ case "summary":
169
+ for (const p of metric.data.value.dataPoints) {
170
+ out.push(summaryPoint(metric, p, common, resourceIdentityFragment));
171
+ }
172
+ break;
173
+ default:
174
+ break;
175
+ }
176
+ }
177
+ };
178
+
179
+ type SeriesCommon = {
180
+ name: string;
181
+ description: string | null;
182
+ unit: string | null;
183
+ scopeName: string | null;
184
+ scopeVersion: string | null;
185
+ serviceName: string | null;
186
+ resourceAttrsJson: string | null;
187
+ };
188
+
189
+ const numberPoint = (
190
+ metric: Metric,
191
+ p: NumberDataPoint,
192
+ type: MetricType,
193
+ common: SeriesCommon,
194
+ resourceIdentityFragment: string,
195
+ ): DecodedMetricPoint => {
196
+ const value =
197
+ p.value.case === "asDouble"
198
+ ? p.value.value
199
+ : p.value.case === "asInt"
200
+ ? Number(p.value.value)
201
+ : null;
202
+ const attrsRecord = p.attributes.length
203
+ ? keyValuesToRecord(p.attributes)
204
+ : null;
205
+ return {
206
+ ...common,
207
+ type,
208
+ isMonotonic: null,
209
+ temporality: null,
210
+ attributesJson: attrsRecord ? JSON.stringify(attrsRecord) : null,
211
+ identity: buildIdentity(
212
+ resourceIdentityFragment,
213
+ common.scopeName,
214
+ metric.name,
215
+ p.attributes,
216
+ ),
217
+ tsNs: p.timeUnixNano.toString(),
218
+ startTsNs: p.startTimeUnixNano > 0n ? p.startTimeUnixNano.toString() : null,
219
+ value,
220
+ count: null,
221
+ sum: null,
222
+ min: null,
223
+ max: null,
224
+ boundsJson: null,
225
+ bucketCountsJson: null,
226
+ extraJson: null,
227
+ exemplarsJson: encodeExemplars(p.exemplars),
228
+ };
229
+ };
230
+
231
+ const histogramPoint = (
232
+ metric: Metric,
233
+ p: HistogramDataPoint,
234
+ common: SeriesCommon,
235
+ resourceIdentityFragment: string,
236
+ ): DecodedMetricPoint => {
237
+ const attrsRecord = p.attributes.length
238
+ ? keyValuesToRecord(p.attributes)
239
+ : null;
240
+ return {
241
+ ...common,
242
+ type: "histogram",
243
+ isMonotonic: null,
244
+ temporality: null,
245
+ attributesJson: attrsRecord ? JSON.stringify(attrsRecord) : null,
246
+ identity: buildIdentity(
247
+ resourceIdentityFragment,
248
+ common.scopeName,
249
+ metric.name,
250
+ p.attributes,
251
+ ),
252
+ tsNs: p.timeUnixNano.toString(),
253
+ startTsNs: p.startTimeUnixNano > 0n ? p.startTimeUnixNano.toString() : null,
254
+ value: null,
255
+ count: Number(p.count),
256
+ sum: p.sum ?? null,
257
+ min: p.min ?? null,
258
+ max: p.max ?? null,
259
+ boundsJson: p.explicitBounds.length
260
+ ? JSON.stringify(p.explicitBounds)
261
+ : null,
262
+ bucketCountsJson: p.bucketCounts.length
263
+ ? JSON.stringify(p.bucketCounts.map((b) => Number(b)))
264
+ : null,
265
+ extraJson: null,
266
+ exemplarsJson: encodeExemplars(p.exemplars),
267
+ };
268
+ };
269
+
270
+ const expHistogramPoint = (
271
+ metric: Metric,
272
+ p: ExponentialHistogramDataPoint,
273
+ common: SeriesCommon,
274
+ resourceIdentityFragment: string,
275
+ ): DecodedMetricPoint => {
276
+ const attrsRecord = p.attributes.length
277
+ ? keyValuesToRecord(p.attributes)
278
+ : null;
279
+ const extra = {
280
+ scale: p.scale,
281
+ zeroCount: Number(p.zeroCount),
282
+ zeroThreshold: p.zeroThreshold,
283
+ positive: p.positive
284
+ ? {
285
+ offset: p.positive.offset,
286
+ bucketCounts: p.positive.bucketCounts.map((b) => Number(b)),
287
+ }
288
+ : null,
289
+ negative: p.negative
290
+ ? {
291
+ offset: p.negative.offset,
292
+ bucketCounts: p.negative.bucketCounts.map((b) => Number(b)),
293
+ }
294
+ : null,
295
+ };
296
+ return {
297
+ ...common,
298
+ type: "exp_histogram",
299
+ isMonotonic: null,
300
+ temporality: null,
301
+ attributesJson: attrsRecord ? JSON.stringify(attrsRecord) : null,
302
+ identity: buildIdentity(
303
+ resourceIdentityFragment,
304
+ common.scopeName,
305
+ metric.name,
306
+ p.attributes,
307
+ ),
308
+ tsNs: p.timeUnixNano.toString(),
309
+ startTsNs: p.startTimeUnixNano > 0n ? p.startTimeUnixNano.toString() : null,
310
+ value: null,
311
+ count: Number(p.count),
312
+ sum: p.sum ?? null,
313
+ min: p.min ?? null,
314
+ max: p.max ?? null,
315
+ boundsJson: null,
316
+ bucketCountsJson: null,
317
+ extraJson: JSON.stringify(extra),
318
+ exemplarsJson: encodeExemplars(p.exemplars),
319
+ };
320
+ };
321
+
322
+ const summaryPoint = (
323
+ metric: Metric,
324
+ p: SummaryDataPoint,
325
+ common: SeriesCommon,
326
+ resourceIdentityFragment: string,
327
+ ): DecodedMetricPoint => {
328
+ const attrsRecord = p.attributes.length
329
+ ? keyValuesToRecord(p.attributes)
330
+ : null;
331
+ const extra = {
332
+ quantileValues: p.quantileValues.map((q) => ({
333
+ quantile: q.quantile,
334
+ value: q.value,
335
+ })),
336
+ };
337
+ return {
338
+ ...common,
339
+ type: "summary",
340
+ isMonotonic: null,
341
+ temporality: null,
342
+ attributesJson: attrsRecord ? JSON.stringify(attrsRecord) : null,
343
+ identity: buildIdentity(
344
+ resourceIdentityFragment,
345
+ common.scopeName,
346
+ metric.name,
347
+ p.attributes,
348
+ ),
349
+ tsNs: p.timeUnixNano.toString(),
350
+ startTsNs: p.startTimeUnixNano > 0n ? p.startTimeUnixNano.toString() : null,
351
+ value: null,
352
+ count: Number(p.count),
353
+ sum: p.sum,
354
+ min: null,
355
+ max: null,
356
+ boundsJson: null,
357
+ bucketCountsJson: null,
358
+ extraJson: JSON.stringify(extra),
359
+ exemplarsJson: null, // Summary has no exemplars in the spec.
360
+ };
361
+ };
362
+
363
+ const encodeExemplars = (exemplars: Exemplar[]): string | null => {
364
+ if (!exemplars.length) return null;
365
+ return JSON.stringify(
366
+ exemplars.map<DecodedExemplar>((e) => ({
367
+ value:
368
+ e.value.case === "asDouble"
369
+ ? e.value.value
370
+ : e.value.case === "asInt"
371
+ ? Number(e.value.value)
372
+ : 0,
373
+ traceId: e.traceId.length === 16 ? bytesToHex(e.traceId) : null,
374
+ spanId: e.spanId.length === 8 ? bytesToHex(e.spanId) : null,
375
+ tsNs: e.timeUnixNano.toString(),
376
+ })),
377
+ );
378
+ };
379
+
380
+ /**
381
+ * Canonical series identity. Stable across encodings (JSON/proto) because
382
+ * attributes are sorted and values normalized. Short enough to index.
383
+ */
384
+ const buildIdentity = (
385
+ resourceFragment: string,
386
+ scopeName: string | null,
387
+ metricName: string,
388
+ attrs: KeyValue[],
389
+ ): string =>
390
+ JSON.stringify([
391
+ resourceFragment,
392
+ scopeName ?? "",
393
+ metricName,
394
+ canonicalKeyValues(attrs),
395
+ ]);
396
+
397
+ const canonicalKeyValues = (attrs: KeyValue[]): string => {
398
+ if (!attrs.length) return "";
399
+ const rec = keyValuesToRecord(attrs);
400
+ const keys = Object.keys(rec).sort();
401
+ return JSON.stringify(keys.map((k) => [k, rec[k]]));
402
+ };
@@ -0,0 +1,92 @@
1
+ import { fromBinary, fromJson } from "@bufbuild/protobuf";
2
+ import type {
3
+ OtlpResourceSpans,
4
+ OtlpTraceExportRequest,
5
+ } from "@obsunified/types";
6
+ import {
7
+ type ExportTraceServiceRequest,
8
+ ExportTraceServiceRequestSchema,
9
+ } from "../gen/opentelemetry/proto/collector/trace/v1/trace_service_pb.js";
10
+ import type { ResourceSpans } from "../gen/opentelemetry/proto/trace/v1/trace_pb.js";
11
+ import { decodeJsonBody, OtlpDecodeError, type ReadBodyResult } from "./body";
12
+ import { adaptKeyValue, bigintToString, bytesToHex } from "./values";
13
+
14
+ /**
15
+ * Decode an OTLP trace export request from wire bytes into the legacy
16
+ * `@obsunified/types` shape consumed by `toStoredSpans`.
17
+ */
18
+ export const decodeTraceRequest = (
19
+ body: ReadBodyResult,
20
+ ): OtlpTraceExportRequest => {
21
+ let msg: ExportTraceServiceRequest;
22
+ try {
23
+ msg =
24
+ body.wireFormat === "protobuf"
25
+ ? fromBinary(ExportTraceServiceRequestSchema, body.bytes)
26
+ : fromJson(ExportTraceServiceRequestSchema, decodeJsonBody(body.bytes));
27
+ } catch (err) {
28
+ throw new OtlpDecodeError(
29
+ `Malformed OTLP body: ${(err as Error).message}`,
30
+ 400,
31
+ );
32
+ }
33
+ return { resourceSpans: msg.resourceSpans.map(adaptResourceSpans) };
34
+ };
35
+
36
+ // ── proto-native → legacy shape adapters ─────────────────────────────
37
+
38
+ const adaptResourceSpans = (rs: ResourceSpans): OtlpResourceSpans => ({
39
+ resource: rs.resource
40
+ ? { attributes: rs.resource.attributes.map(adaptKeyValue) }
41
+ : undefined,
42
+ scopeSpans: rs.scopeSpans.map((ss) => ({
43
+ scope: ss.scope
44
+ ? { name: ss.scope.name, version: ss.scope.version }
45
+ : undefined,
46
+ spans: ss.spans.flatMap((s) => {
47
+ if (s.traceId.length !== 16 || s.spanId.length !== 8) return [];
48
+ return [
49
+ {
50
+ traceId: bytesToHex(s.traceId),
51
+ spanId: bytesToHex(s.spanId),
52
+ parentSpanId:
53
+ s.parentSpanId.length === 8
54
+ ? bytesToHex(s.parentSpanId)
55
+ : undefined,
56
+ traceState: s.traceState || undefined,
57
+ name: s.name,
58
+ kind: s.kind,
59
+ startTimeUnixNano: bigintToString(s.startTimeUnixNano),
60
+ endTimeUnixNano: bigintToString(s.endTimeUnixNano),
61
+ attributes: s.attributes.map(adaptKeyValue),
62
+ droppedAttributesCount: s.droppedAttributesCount || undefined,
63
+ events: s.events.map((e) => ({
64
+ name: e.name,
65
+ timeUnixNano: bigintToString(e.timeUnixNano),
66
+ attributes: e.attributes.map(adaptKeyValue),
67
+ droppedAttributesCount: e.droppedAttributesCount || undefined,
68
+ })),
69
+ droppedEventsCount: s.droppedEventsCount || undefined,
70
+ links: s.links.flatMap((l) =>
71
+ l.traceId.length === 16 && l.spanId.length === 8
72
+ ? [
73
+ {
74
+ traceId: bytesToHex(l.traceId),
75
+ spanId: bytesToHex(l.spanId),
76
+ traceState: l.traceState || undefined,
77
+ attributes: l.attributes.map(adaptKeyValue),
78
+ droppedAttributesCount:
79
+ l.droppedAttributesCount || undefined,
80
+ },
81
+ ]
82
+ : [],
83
+ ),
84
+ droppedLinksCount: s.droppedLinksCount || undefined,
85
+ status: s.status
86
+ ? { code: s.status.code, message: s.status.message }
87
+ : undefined,
88
+ },
89
+ ];
90
+ }),
91
+ })),
92
+ });