@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,800 @@
1
+ import { create, toBinary, toJson } from "@bufbuild/protobuf";
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ decodeLogsRequest,
5
+ decodeMetricsRequest,
6
+ decodeTraceRequest,
7
+ OtlpDecodeError,
8
+ readOtlpBody,
9
+ } from "./decode";
10
+ import { ExportLogsServiceRequestSchema } from "./gen/opentelemetry/proto/collector/logs/v1/logs_service_pb.js";
11
+ import { ExportMetricsServiceRequestSchema } from "./gen/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.js";
12
+ import { ExportTraceServiceRequestSchema } from "./gen/opentelemetry/proto/collector/trace/v1/trace_service_pb.js";
13
+ import {
14
+ AnyValueSchema,
15
+ KeyValueSchema,
16
+ } from "./gen/opentelemetry/proto/common/v1/common_pb.js";
17
+ import {
18
+ LogRecordSchema,
19
+ ResourceLogsSchema,
20
+ ScopeLogsSchema,
21
+ SeverityNumber,
22
+ } from "./gen/opentelemetry/proto/logs/v1/logs_pb.js";
23
+ import {
24
+ AggregationTemporality,
25
+ ExemplarSchema,
26
+ ExponentialHistogramDataPoint_BucketsSchema,
27
+ ExponentialHistogramDataPointSchema,
28
+ ExponentialHistogramSchema,
29
+ GaugeSchema,
30
+ HistogramDataPointSchema,
31
+ HistogramSchema,
32
+ MetricSchema,
33
+ NumberDataPointSchema,
34
+ ResourceMetricsSchema,
35
+ ScopeMetricsSchema,
36
+ SummaryDataPoint_ValueAtQuantileSchema,
37
+ SummaryDataPointSchema,
38
+ SummarySchema,
39
+ SumSchema,
40
+ } from "./gen/opentelemetry/proto/metrics/v1/metrics_pb.js";
41
+ import { ResourceSchema } from "./gen/opentelemetry/proto/resource/v1/resource_pb.js";
42
+ import {
43
+ ResourceSpansSchema,
44
+ ScopeSpansSchema,
45
+ SpanSchema,
46
+ } from "./gen/opentelemetry/proto/trace/v1/trace_pb.js";
47
+
48
+ const TRACE_ID = new Uint8Array([
49
+ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
50
+ 0x0e, 0x0f, 0x10,
51
+ ]);
52
+ const SPAN_ID = new Uint8Array([
53
+ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11,
54
+ ]);
55
+ const TRACE_HEX = "0102030405060708090a0b0c0d0e0f10";
56
+ const SPAN_HEX = "aabbccddeeff0011";
57
+
58
+ const buildMessage = () =>
59
+ create(ExportTraceServiceRequestSchema, {
60
+ resourceSpans: [
61
+ create(ResourceSpansSchema, {
62
+ scopeSpans: [
63
+ create(ScopeSpansSchema, {
64
+ spans: [
65
+ create(SpanSchema, {
66
+ traceId: TRACE_ID,
67
+ spanId: SPAN_ID,
68
+ name: "GET /api/users",
69
+ startTimeUnixNano: 1_700_000_000_000_000_000n,
70
+ endTimeUnixNano: 1_700_000_000_100_000_000n,
71
+ }),
72
+ ],
73
+ }),
74
+ ],
75
+ }),
76
+ ],
77
+ });
78
+
79
+ const mockContext = (opts: {
80
+ bytes: ArrayBuffer | Uint8Array;
81
+ contentType: string;
82
+ contentEncoding?: string;
83
+ }) => {
84
+ const buf =
85
+ opts.bytes instanceof Uint8Array
86
+ ? opts.bytes.buffer.slice(
87
+ opts.bytes.byteOffset,
88
+ opts.bytes.byteOffset + opts.bytes.byteLength,
89
+ )
90
+ : opts.bytes;
91
+ return {
92
+ req: {
93
+ arrayBuffer: async () => buf,
94
+ header: (name: string) => {
95
+ const n = name.toLowerCase();
96
+ if (n === "content-type") return opts.contentType;
97
+ if (n === "content-encoding") return opts.contentEncoding;
98
+ return undefined;
99
+ },
100
+ },
101
+ } as unknown as Parameters<typeof readOtlpBody>[0];
102
+ };
103
+
104
+ describe("readOtlpBody", () => {
105
+ it("rejects missing content-type with 415", async () => {
106
+ const c = mockContext({ bytes: new Uint8Array(), contentType: "" });
107
+ await expect(readOtlpBody(c)).rejects.toBeInstanceOf(OtlpDecodeError);
108
+ });
109
+
110
+ it("rejects unknown content-encoding with 415", async () => {
111
+ const c = mockContext({
112
+ bytes: new Uint8Array([1, 2, 3]),
113
+ contentType: "application/x-protobuf",
114
+ contentEncoding: "brotli",
115
+ });
116
+ await expect(readOtlpBody(c)).rejects.toMatchObject({ status: 415 });
117
+ });
118
+
119
+ it("detects protobuf wire format", async () => {
120
+ const c = mockContext({
121
+ bytes: new Uint8Array(),
122
+ contentType: "application/x-protobuf",
123
+ });
124
+ const { wireFormat } = await readOtlpBody(c);
125
+ expect(wireFormat).toBe("protobuf");
126
+ });
127
+
128
+ it("detects json wire format", async () => {
129
+ const c = mockContext({
130
+ bytes: new Uint8Array(),
131
+ contentType: "application/json",
132
+ });
133
+ const { wireFormat } = await readOtlpBody(c);
134
+ expect(wireFormat).toBe("json");
135
+ });
136
+
137
+ it("decompresses gzip bodies", async () => {
138
+ const raw = new TextEncoder().encode(JSON.stringify({ resourceSpans: [] }));
139
+ const gz = await new Response(
140
+ new Response(raw).body?.pipeThrough(new CompressionStream("gzip")),
141
+ ).arrayBuffer();
142
+ const c = mockContext({
143
+ bytes: gz,
144
+ contentType: "application/json",
145
+ contentEncoding: "gzip",
146
+ });
147
+ const { bytes } = await readOtlpBody(c);
148
+ expect(new TextDecoder().decode(bytes)).toBe('{"resourceSpans":[]}');
149
+ });
150
+ });
151
+
152
+ describe("decodeTraceRequest", () => {
153
+ it("round-trips a span via protobuf", () => {
154
+ const bytes = toBinary(ExportTraceServiceRequestSchema, buildMessage());
155
+ const decoded = decodeTraceRequest({ bytes, wireFormat: "protobuf" });
156
+ const span = decoded.resourceSpans?.[0]?.scopeSpans?.[0]?.spans?.[0];
157
+ expect(span?.traceId).toBe(TRACE_HEX);
158
+ expect(span?.spanId).toBe(SPAN_HEX);
159
+ expect(span?.name).toBe("GET /api/users");
160
+ expect(span?.startTimeUnixNano).toBe("1700000000000000000");
161
+ expect(span?.endTimeUnixNano).toBe("1700000000100000000");
162
+ });
163
+
164
+ it("round-trips a span via JSON", () => {
165
+ const json = toJson(ExportTraceServiceRequestSchema, buildMessage());
166
+ const bytes = new TextEncoder().encode(JSON.stringify(json));
167
+ const decoded = decodeTraceRequest({ bytes, wireFormat: "json" });
168
+ const span = decoded.resourceSpans?.[0]?.scopeSpans?.[0]?.spans?.[0];
169
+ expect(span?.traceId).toBe(TRACE_HEX);
170
+ expect(span?.spanId).toBe(SPAN_HEX);
171
+ expect(span?.startTimeUnixNano).toBe("1700000000000000000");
172
+ });
173
+
174
+ it("throws 400 on malformed protobuf", () => {
175
+ expect(() =>
176
+ decodeTraceRequest({
177
+ bytes: new Uint8Array([0xff, 0xff, 0xff]),
178
+ wireFormat: "protobuf",
179
+ }),
180
+ ).toThrowError(OtlpDecodeError);
181
+ });
182
+
183
+ it("throws 400 on malformed JSON", () => {
184
+ expect(() =>
185
+ decodeTraceRequest({
186
+ bytes: new TextEncoder().encode("{not json"),
187
+ wireFormat: "json",
188
+ }),
189
+ ).toThrowError(OtlpDecodeError);
190
+ });
191
+
192
+ it("accepts hex trace/span IDs in JSON (OTLP-spec form, not base64)", () => {
193
+ // Real OTel SDKs emit IDs as lowercase hex strings per the OTLP-JSON
194
+ // spec, not base64 (which is the proto-JSON default for `bytes` fields).
195
+ // The Go reference receiver accepts both — so must we.
196
+ const body = JSON.stringify({
197
+ resourceSpans: [
198
+ {
199
+ scopeSpans: [
200
+ {
201
+ spans: [
202
+ {
203
+ traceId: TRACE_HEX,
204
+ spanId: SPAN_HEX,
205
+ parentSpanId: "1122334455667788",
206
+ name: "hex-id",
207
+ startTimeUnixNano: "1700000000000000000",
208
+ endTimeUnixNano: "1700000000100000000",
209
+ },
210
+ ],
211
+ },
212
+ ],
213
+ },
214
+ ],
215
+ });
216
+ const decoded = decodeTraceRequest({
217
+ bytes: new TextEncoder().encode(body),
218
+ wireFormat: "json",
219
+ });
220
+ const span = decoded.resourceSpans?.[0]?.scopeSpans?.[0]?.spans?.[0];
221
+ expect(span?.traceId).toBe(TRACE_HEX);
222
+ expect(span?.spanId).toBe(SPAN_HEX);
223
+ expect(span?.parentSpanId).toBe("1122334455667788");
224
+ });
225
+
226
+ it("drops spans with malformed IDs instead of producing invalid hex", () => {
227
+ const body = JSON.stringify({
228
+ resourceSpans: [
229
+ {
230
+ scopeSpans: [
231
+ {
232
+ spans: [
233
+ {
234
+ traceId: "abc",
235
+ spanId: "def",
236
+ name: "bad-id",
237
+ startTimeUnixNano: "1700000000000000000",
238
+ endTimeUnixNano: "1700000000100000000",
239
+ },
240
+ ],
241
+ },
242
+ ],
243
+ },
244
+ ],
245
+ });
246
+ const decoded = decodeTraceRequest({
247
+ bytes: new TextEncoder().encode(body),
248
+ wireFormat: "json",
249
+ });
250
+ expect(decoded.resourceSpans?.[0]?.scopeSpans?.[0]?.spans).toEqual([]);
251
+ });
252
+ });
253
+
254
+ // ── Logs ────────────────────────────────────────────────────────────────
255
+
256
+ const LOG_TRACE_ID = new Uint8Array([
257
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
258
+ 0xee, 0xff, 0x00,
259
+ ]);
260
+ const LOG_SPAN_ID = new Uint8Array([
261
+ 0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe,
262
+ ]);
263
+
264
+ const buildLogsMessage = () =>
265
+ create(ExportLogsServiceRequestSchema, {
266
+ resourceLogs: [
267
+ create(ResourceLogsSchema, {
268
+ resource: create(ResourceSchema, {
269
+ attributes: [
270
+ create(KeyValueSchema, {
271
+ key: "service.name",
272
+ value: create(AnyValueSchema, {
273
+ value: { case: "stringValue", value: "checkout" },
274
+ }),
275
+ }),
276
+ ],
277
+ }),
278
+ scopeLogs: [
279
+ create(ScopeLogsSchema, {
280
+ scope: { name: "users", version: "1.0" },
281
+ logRecords: [
282
+ create(LogRecordSchema, {
283
+ timeUnixNano: 1_700_000_000_000_000_000n,
284
+ observedTimeUnixNano: 1_700_000_000_000_000_000n,
285
+ severityNumber: SeverityNumber.ERROR,
286
+ severityText: "ERROR",
287
+ body: create(AnyValueSchema, {
288
+ value: { case: "stringValue", value: "boom" },
289
+ }),
290
+ traceId: LOG_TRACE_ID,
291
+ spanId: LOG_SPAN_ID,
292
+ attributes: [
293
+ create(KeyValueSchema, {
294
+ key: "request_id",
295
+ value: create(AnyValueSchema, {
296
+ value: { case: "stringValue", value: "abc" },
297
+ }),
298
+ }),
299
+ ],
300
+ }),
301
+ ],
302
+ }),
303
+ ],
304
+ }),
305
+ ],
306
+ });
307
+
308
+ describe("decodeLogsRequest", () => {
309
+ it("round-trips an OTLP log via protobuf", () => {
310
+ const bytes = toBinary(ExportLogsServiceRequestSchema, buildLogsMessage());
311
+ const [log] = decodeLogsRequest({ bytes, wireFormat: "protobuf" });
312
+ expect(log).toBeDefined();
313
+ expect(log?.serviceName).toBe("checkout");
314
+ expect(log?.loggerName).toBe("users");
315
+ expect(log?.severity).toBe("ERROR");
316
+ expect(log?.severityNumber).toBe(17);
317
+ expect(log?.message).toBe("boom");
318
+ expect(log?.traceId).toBe("112233445566778899aabbccddeeff00");
319
+ expect(log?.spanId).toBe("deadbeefcafebabe");
320
+ expect(log?.attributes).toEqual({ request_id: "abc" });
321
+ expect(log?.occurredAt).toBe("2023-11-14T22:13:20.000Z");
322
+ });
323
+
324
+ it("round-trips an OTLP log via JSON", () => {
325
+ const json = toJson(ExportLogsServiceRequestSchema, buildLogsMessage());
326
+ const bytes = new TextEncoder().encode(JSON.stringify(json));
327
+ const [log] = decodeLogsRequest({ bytes, wireFormat: "json" });
328
+ expect(log?.serviceName).toBe("checkout");
329
+ expect(log?.severity).toBe("ERROR");
330
+ expect(log?.message).toBe("boom");
331
+ expect(log?.traceId).toBe("112233445566778899aabbccddeeff00");
332
+ });
333
+
334
+ it("ignores malformed JSON timestamps instead of rejecting the batch", () => {
335
+ const body = JSON.stringify({
336
+ resourceLogs: [
337
+ {
338
+ scopeLogs: [
339
+ {
340
+ logRecords: [
341
+ {
342
+ timeUnixNano: "not-a-number",
343
+ body: { stringValue: "timestamp survives" },
344
+ },
345
+ ],
346
+ },
347
+ ],
348
+ },
349
+ ],
350
+ });
351
+ const [log] = decodeLogsRequest({
352
+ bytes: new TextEncoder().encode(body),
353
+ wireFormat: "json",
354
+ });
355
+ expect(log?.message).toBe("timestamp survives");
356
+ expect(() => new Date(log?.occurredAt ?? "").toISOString()).not.toThrow();
357
+ });
358
+
359
+ it("maps severity number to LogSeverity buckets", () => {
360
+ const mk = (n: number) =>
361
+ create(ExportLogsServiceRequestSchema, {
362
+ resourceLogs: [
363
+ create(ResourceLogsSchema, {
364
+ scopeLogs: [
365
+ create(ScopeLogsSchema, {
366
+ logRecords: [
367
+ create(LogRecordSchema, {
368
+ severityNumber: n as SeverityNumber,
369
+ body: create(AnyValueSchema, {
370
+ value: { case: "stringValue", value: "x" },
371
+ }),
372
+ }),
373
+ ],
374
+ }),
375
+ ],
376
+ }),
377
+ ],
378
+ });
379
+
380
+ const sev = (n: number) =>
381
+ decodeLogsRequest({
382
+ bytes: toBinary(ExportLogsServiceRequestSchema, mk(n)),
383
+ wireFormat: "protobuf",
384
+ })[0]?.severity;
385
+
386
+ expect(sev(5)).toBe("DEBUG");
387
+ expect(sev(9)).toBe("INFO");
388
+ expect(sev(13)).toBe("WARN");
389
+ expect(sev(17)).toBe("ERROR");
390
+ expect(sev(21)).toBe("FATAL");
391
+ });
392
+
393
+ it("falls back to severityText when severityNumber is unset", () => {
394
+ const msg = create(ExportLogsServiceRequestSchema, {
395
+ resourceLogs: [
396
+ create(ResourceLogsSchema, {
397
+ scopeLogs: [
398
+ create(ScopeLogsSchema, {
399
+ logRecords: [
400
+ create(LogRecordSchema, {
401
+ severityText: "warn",
402
+ body: create(AnyValueSchema, {
403
+ value: { case: "stringValue", value: "x" },
404
+ }),
405
+ }),
406
+ ],
407
+ }),
408
+ ],
409
+ }),
410
+ ],
411
+ });
412
+ const [log] = decodeLogsRequest({
413
+ bytes: toBinary(ExportLogsServiceRequestSchema, msg),
414
+ wireFormat: "protobuf",
415
+ });
416
+ expect(log?.severity).toBe("WARN");
417
+ });
418
+
419
+ it("returns empty trace/span IDs as null", () => {
420
+ const msg = create(ExportLogsServiceRequestSchema, {
421
+ resourceLogs: [
422
+ create(ResourceLogsSchema, {
423
+ scopeLogs: [
424
+ create(ScopeLogsSchema, {
425
+ logRecords: [
426
+ create(LogRecordSchema, {
427
+ severityNumber: SeverityNumber.INFO,
428
+ body: create(AnyValueSchema, {
429
+ value: { case: "stringValue", value: "x" },
430
+ }),
431
+ }),
432
+ ],
433
+ }),
434
+ ],
435
+ }),
436
+ ],
437
+ });
438
+ const [log] = decodeLogsRequest({
439
+ bytes: toBinary(ExportLogsServiceRequestSchema, msg),
440
+ wireFormat: "protobuf",
441
+ });
442
+ expect(log?.traceId).toBeNull();
443
+ expect(log?.spanId).toBeNull();
444
+ });
445
+ });
446
+
447
+ // ── Metrics ─────────────────────────────────────────────────────────────
448
+
449
+ const METRIC_TRACE_ID = new Uint8Array(16).fill(1);
450
+ const METRIC_SPAN_ID = new Uint8Array(8).fill(2);
451
+
452
+ const buildMetricsMessage = () =>
453
+ create(ExportMetricsServiceRequestSchema, {
454
+ resourceMetrics: [
455
+ create(ResourceMetricsSchema, {
456
+ resource: create(ResourceSchema, {
457
+ attributes: [
458
+ create(KeyValueSchema, {
459
+ key: "service.name",
460
+ value: create(AnyValueSchema, {
461
+ value: { case: "stringValue", value: "checkout" },
462
+ }),
463
+ }),
464
+ ],
465
+ }),
466
+ scopeMetrics: [
467
+ create(ScopeMetricsSchema, {
468
+ scope: { name: "http", version: "1.0" },
469
+ metrics: [
470
+ create(MetricSchema, {
471
+ name: "http.server.active_requests",
472
+ unit: "1",
473
+ description: "currently in-flight",
474
+ data: {
475
+ case: "gauge",
476
+ value: create(GaugeSchema, {
477
+ dataPoints: [
478
+ create(NumberDataPointSchema, {
479
+ timeUnixNano: 1_700_000_000_000_000_000n,
480
+ value: { case: "asDouble", value: 42.5 },
481
+ attributes: [
482
+ create(KeyValueSchema, {
483
+ key: "method",
484
+ value: create(AnyValueSchema, {
485
+ value: { case: "stringValue", value: "GET" },
486
+ }),
487
+ }),
488
+ ],
489
+ }),
490
+ ],
491
+ }),
492
+ },
493
+ }),
494
+ create(MetricSchema, {
495
+ name: "http.server.requests",
496
+ data: {
497
+ case: "sum",
498
+ value: create(SumSchema, {
499
+ isMonotonic: true,
500
+ aggregationTemporality: AggregationTemporality.CUMULATIVE,
501
+ dataPoints: [
502
+ create(NumberDataPointSchema, {
503
+ timeUnixNano: 1_700_000_000_000_000_000n,
504
+ startTimeUnixNano: 1_699_999_000_000_000_000n,
505
+ value: { case: "asInt", value: 128n },
506
+ exemplars: [
507
+ create(ExemplarSchema, {
508
+ timeUnixNano: 1_700_000_000_000_000_000n,
509
+ value: { case: "asDouble", value: 1 },
510
+ traceId: METRIC_TRACE_ID,
511
+ spanId: METRIC_SPAN_ID,
512
+ }),
513
+ ],
514
+ }),
515
+ ],
516
+ }),
517
+ },
518
+ }),
519
+ create(MetricSchema, {
520
+ name: "http.server.duration",
521
+ unit: "ms",
522
+ data: {
523
+ case: "histogram",
524
+ value: create(HistogramSchema, {
525
+ aggregationTemporality: AggregationTemporality.DELTA,
526
+ dataPoints: [
527
+ create(HistogramDataPointSchema, {
528
+ timeUnixNano: 1_700_000_000_000_000_000n,
529
+ count: 10n,
530
+ sum: 250,
531
+ min: 5,
532
+ max: 90,
533
+ explicitBounds: [10, 50, 100],
534
+ bucketCounts: [3n, 4n, 2n, 1n],
535
+ }),
536
+ ],
537
+ }),
538
+ },
539
+ }),
540
+ ],
541
+ }),
542
+ ],
543
+ }),
544
+ ],
545
+ });
546
+
547
+ describe("decodeMetricsRequest", () => {
548
+ it("decodes gauge, sum, and histogram via protobuf", () => {
549
+ const bytes = toBinary(
550
+ ExportMetricsServiceRequestSchema,
551
+ buildMetricsMessage(),
552
+ );
553
+ const points = decodeMetricsRequest({ bytes, wireFormat: "protobuf" });
554
+ expect(points).toHaveLength(3);
555
+
556
+ const gauge = points.find((p) => p.type === "gauge");
557
+ expect(gauge?.name).toBe("http.server.active_requests");
558
+ expect(gauge?.serviceName).toBe("checkout");
559
+ expect(gauge?.scopeName).toBe("http");
560
+ expect(gauge?.value).toBe(42.5);
561
+ expect(gauge?.unit).toBe("1");
562
+ expect(gauge?.tsNs).toBe("1700000000000000000");
563
+ expect(gauge?.attributesJson).toBe('{"method":"GET"}');
564
+
565
+ const sum = points.find((p) => p.type === "sum");
566
+ expect(sum?.value).toBe(128);
567
+ expect(sum?.isMonotonic).toBe(true);
568
+ expect(sum?.temporality).toBe(AggregationTemporality.CUMULATIVE);
569
+ expect(sum?.startTsNs).toBe("1699999000000000000");
570
+ const exemplars = sum?.exemplarsJson ? JSON.parse(sum.exemplarsJson) : [];
571
+ expect(exemplars[0].value).toBe(1);
572
+ expect(exemplars[0].traceId).toBe("01010101010101010101010101010101");
573
+ expect(exemplars[0].spanId).toBe("0202020202020202");
574
+
575
+ const hist = points.find((p) => p.type === "histogram");
576
+ expect(hist?.count).toBe(10);
577
+ expect(hist?.sum).toBe(250);
578
+ expect(hist?.min).toBe(5);
579
+ expect(hist?.max).toBe(90);
580
+ expect(hist?.boundsJson).toBe("[10,50,100]");
581
+ expect(hist?.bucketCountsJson).toBe("[3,4,2,1]");
582
+ expect(hist?.temporality).toBe(AggregationTemporality.DELTA);
583
+ });
584
+
585
+ it("decodes via JSON identically to protobuf", () => {
586
+ const json = toJson(
587
+ ExportMetricsServiceRequestSchema,
588
+ buildMetricsMessage(),
589
+ );
590
+ const bytes = new TextEncoder().encode(JSON.stringify(json));
591
+ const points = decodeMetricsRequest({ bytes, wireFormat: "json" });
592
+ expect(points).toHaveLength(3);
593
+ const gauge = points.find((p) => p.type === "gauge");
594
+ expect(gauge?.value).toBe(42.5);
595
+ });
596
+
597
+ it("produces deterministic identity across identical payloads", () => {
598
+ const a = decodeMetricsRequest({
599
+ bytes: toBinary(ExportMetricsServiceRequestSchema, buildMetricsMessage()),
600
+ wireFormat: "protobuf",
601
+ });
602
+ const b = decodeMetricsRequest({
603
+ bytes: toBinary(ExportMetricsServiceRequestSchema, buildMetricsMessage()),
604
+ wireFormat: "protobuf",
605
+ });
606
+ expect(a.map((p) => p.identity)).toEqual(b.map((p) => p.identity));
607
+ });
608
+
609
+ it("gives different identity for different attributes", () => {
610
+ const mk = (method: string) =>
611
+ create(ExportMetricsServiceRequestSchema, {
612
+ resourceMetrics: [
613
+ create(ResourceMetricsSchema, {
614
+ scopeMetrics: [
615
+ create(ScopeMetricsSchema, {
616
+ metrics: [
617
+ create(MetricSchema, {
618
+ name: "x",
619
+ data: {
620
+ case: "gauge",
621
+ value: create(GaugeSchema, {
622
+ dataPoints: [
623
+ create(NumberDataPointSchema, {
624
+ timeUnixNano: 1n,
625
+ value: { case: "asDouble", value: 1 },
626
+ attributes: [
627
+ create(KeyValueSchema, {
628
+ key: "method",
629
+ value: create(AnyValueSchema, {
630
+ value: {
631
+ case: "stringValue",
632
+ value: method,
633
+ },
634
+ }),
635
+ }),
636
+ ],
637
+ }),
638
+ ],
639
+ }),
640
+ },
641
+ }),
642
+ ],
643
+ }),
644
+ ],
645
+ }),
646
+ ],
647
+ });
648
+ const [getP] = decodeMetricsRequest({
649
+ bytes: toBinary(ExportMetricsServiceRequestSchema, mk("GET")),
650
+ wireFormat: "protobuf",
651
+ });
652
+ const [postP] = decodeMetricsRequest({
653
+ bytes: toBinary(ExportMetricsServiceRequestSchema, mk("POST")),
654
+ wireFormat: "protobuf",
655
+ });
656
+ expect(getP?.identity).not.toEqual(postP?.identity);
657
+ });
658
+
659
+ it("returns empty array for metric with no data", () => {
660
+ const msg = create(ExportMetricsServiceRequestSchema, {
661
+ resourceMetrics: [
662
+ create(ResourceMetricsSchema, {
663
+ scopeMetrics: [
664
+ create(ScopeMetricsSchema, {
665
+ metrics: [create(MetricSchema, { name: "nope" })],
666
+ }),
667
+ ],
668
+ }),
669
+ ],
670
+ });
671
+ const points = decodeMetricsRequest({
672
+ bytes: toBinary(ExportMetricsServiceRequestSchema, msg),
673
+ wireFormat: "protobuf",
674
+ });
675
+ expect(points).toEqual([]);
676
+ });
677
+
678
+ it("decodes exponential histogram with positive/negative buckets", () => {
679
+ const msg = create(ExportMetricsServiceRequestSchema, {
680
+ resourceMetrics: [
681
+ create(ResourceMetricsSchema, {
682
+ scopeMetrics: [
683
+ create(ScopeMetricsSchema, {
684
+ metrics: [
685
+ create(MetricSchema, {
686
+ name: "latency.exp",
687
+ unit: "ms",
688
+ data: {
689
+ case: "exponentialHistogram",
690
+ value: create(ExponentialHistogramSchema, {
691
+ aggregationTemporality: AggregationTemporality.CUMULATIVE,
692
+ dataPoints: [
693
+ create(ExponentialHistogramDataPointSchema, {
694
+ timeUnixNano: 2_000_000_000_000_000_000n,
695
+ count: 100n,
696
+ sum: 5432,
697
+ min: 1,
698
+ max: 999,
699
+ scale: 3,
700
+ zeroCount: 5n,
701
+ zeroThreshold: 0.001,
702
+ positive: create(
703
+ ExponentialHistogramDataPoint_BucketsSchema,
704
+ {
705
+ offset: -5,
706
+ bucketCounts: [10n, 20n, 30n, 40n],
707
+ },
708
+ ),
709
+ negative: create(
710
+ ExponentialHistogramDataPoint_BucketsSchema,
711
+ { offset: 0, bucketCounts: [] },
712
+ ),
713
+ }),
714
+ ],
715
+ }),
716
+ },
717
+ }),
718
+ ],
719
+ }),
720
+ ],
721
+ }),
722
+ ],
723
+ });
724
+ const [p] = decodeMetricsRequest({
725
+ bytes: toBinary(ExportMetricsServiceRequestSchema, msg),
726
+ wireFormat: "protobuf",
727
+ });
728
+ expect(p?.type).toBe("exp_histogram");
729
+ expect(p?.count).toBe(100);
730
+ expect(p?.sum).toBe(5432);
731
+ expect(p?.min).toBe(1);
732
+ expect(p?.max).toBe(999);
733
+ expect(p?.temporality).toBe(AggregationTemporality.CUMULATIVE);
734
+ const extra = JSON.parse(p?.extraJson ?? "{}");
735
+ expect(extra.scale).toBe(3);
736
+ expect(extra.zeroCount).toBe(5);
737
+ expect(extra.zeroThreshold).toBe(0.001);
738
+ expect(extra.positive.offset).toBe(-5);
739
+ expect(extra.positive.bucketCounts).toEqual([10, 20, 30, 40]);
740
+ expect(extra.negative.offset).toBe(0);
741
+ });
742
+
743
+ it("decodes summary with quantileValues", () => {
744
+ const msg = create(ExportMetricsServiceRequestSchema, {
745
+ resourceMetrics: [
746
+ create(ResourceMetricsSchema, {
747
+ scopeMetrics: [
748
+ create(ScopeMetricsSchema, {
749
+ metrics: [
750
+ create(MetricSchema, {
751
+ name: "request.duration.summary",
752
+ data: {
753
+ case: "summary",
754
+ value: create(SummarySchema, {
755
+ dataPoints: [
756
+ create(SummaryDataPointSchema, {
757
+ timeUnixNano: 3_000_000_000_000_000_000n,
758
+ count: 1000n,
759
+ sum: 45678,
760
+ quantileValues: [
761
+ create(SummaryDataPoint_ValueAtQuantileSchema, {
762
+ quantile: 0.5,
763
+ value: 40,
764
+ }),
765
+ create(SummaryDataPoint_ValueAtQuantileSchema, {
766
+ quantile: 0.95,
767
+ value: 120,
768
+ }),
769
+ create(SummaryDataPoint_ValueAtQuantileSchema, {
770
+ quantile: 0.99,
771
+ value: 250,
772
+ }),
773
+ ],
774
+ }),
775
+ ],
776
+ }),
777
+ },
778
+ }),
779
+ ],
780
+ }),
781
+ ],
782
+ }),
783
+ ],
784
+ });
785
+ const [p] = decodeMetricsRequest({
786
+ bytes: toBinary(ExportMetricsServiceRequestSchema, msg),
787
+ wireFormat: "protobuf",
788
+ });
789
+ expect(p?.type).toBe("summary");
790
+ expect(p?.count).toBe(1000);
791
+ expect(p?.sum).toBe(45678);
792
+ expect(p?.value).toBeNull();
793
+ const extra = JSON.parse(p?.extraJson ?? "{}");
794
+ expect(extra.quantileValues).toEqual([
795
+ { quantile: 0.5, value: 40 },
796
+ { quantile: 0.95, value: 120 },
797
+ { quantile: 0.99, value: 250 },
798
+ ]);
799
+ });
800
+ });