@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,11 @@
1
+ -- AI debugging evidence for side-effecting tool calls.
2
+ --
3
+ -- These fields are opt-in evidence channels. They intentionally store only
4
+ -- explicit audit/diff envelopes emitted by instrumentation, not raw tool
5
+ -- arguments, results, or transport metadata.
6
+
7
+ ALTER TABLE tool_calls ADD COLUMN IF NOT EXISTS mcp_audit_json JSONB;
8
+ ALTER TABLE tool_calls ADD COLUMN IF NOT EXISTS mutation_before_json JSONB;
9
+ ALTER TABLE tool_calls ADD COLUMN IF NOT EXISTS mutation_after_json JSONB;
10
+ ALTER TABLE tool_calls ADD COLUMN IF NOT EXISTS mutation_diff_json JSONB;
11
+ ALTER TABLE tool_calls ADD COLUMN IF NOT EXISTS mutation_artifact_id TEXT;
@@ -0,0 +1,11 @@
1
+ -- Drop the materialized trace instrumentation gaps table.
2
+ --
3
+ -- Trace instrumentation gaps are now computed lazily at read time from the
4
+ -- trace's spans (see `getTelemetryTraceGaps` in lib/store/trace-detail.ts),
5
+ -- rather than being materialized on the ingest hot path. The table and its
6
+ -- index are therefore unused: nothing writes to them, and reads compute gaps
7
+ -- on demand. `DROP TABLE` removes the dependent index; the explicit
8
+ -- `DROP INDEX` mirrors the D1 sibling migration.
9
+
10
+ DROP INDEX IF EXISTS idx_trace_gaps_duration;
11
+ DROP TABLE IF EXISTS trace_instrumentation_gaps;
@@ -0,0 +1,47 @@
1
+ -- RFC 0011 — materialized evidence retrieval refs and expansion telemetry.
2
+
3
+ CREATE TABLE IF NOT EXISTS evidence_retrieval_refs (
4
+ ref_id TEXT PRIMARY KEY,
5
+ project_id TEXT NOT NULL,
6
+ kind TEXT NOT NULL,
7
+ anchor_kind TEXT NOT NULL,
8
+ anchor_id TEXT NOT NULL,
9
+ source TEXT NOT NULL,
10
+ query_json JSONB,
11
+ compacted_from_json JSONB,
12
+ returned_json JSONB,
13
+ issued_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
14
+ last_seen_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
15
+ expires_at TIMESTAMPTZ
16
+ );
17
+
18
+ CREATE INDEX IF NOT EXISTS idx_evidence_refs_project_seen
19
+ ON evidence_retrieval_refs (project_id, last_seen_at DESC);
20
+
21
+ CREATE INDEX IF NOT EXISTS idx_evidence_refs_project_kind
22
+ ON evidence_retrieval_refs (project_id, kind, last_seen_at DESC);
23
+
24
+ CREATE INDEX IF NOT EXISTS idx_evidence_refs_project_anchor
25
+ ON evidence_retrieval_refs (project_id, anchor_kind, anchor_id);
26
+
27
+ CREATE TABLE IF NOT EXISTS evidence_ref_expansions (
28
+ id TEXT PRIMARY KEY,
29
+ ref_id TEXT NOT NULL,
30
+ project_id TEXT NOT NULL,
31
+ kind TEXT NOT NULL,
32
+ source TEXT,
33
+ operation TEXT NOT NULL,
34
+ result_status TEXT NOT NULL,
35
+ limit_value INTEGER,
36
+ query_text TEXT,
37
+ expanded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
38
+ );
39
+
40
+ CREATE INDEX IF NOT EXISTS idx_evidence_expansions_project_time
41
+ ON evidence_ref_expansions (project_id, expanded_at DESC);
42
+
43
+ CREATE INDEX IF NOT EXISTS idx_evidence_expansions_project_ref
44
+ ON evidence_ref_expansions (project_id, ref_id, expanded_at DESC);
45
+
46
+ CREATE INDEX IF NOT EXISTS idx_evidence_expansions_project_kind
47
+ ON evidence_ref_expansions (project_id, kind, expanded_at DESC);
@@ -0,0 +1,40 @@
1
+ # Postgres migrations
2
+
3
+ Companion migrations for the Postgres adapter (`PostgresAdapter`). The canonical
4
+ schema lives in `../migrations/` as SQLite/D1; this directory mirrors each
5
+ numbered migration translated for Postgres semantics.
6
+
7
+ ## Translation rules
8
+
9
+ Mechanical rewrites from SQLite → Postgres:
10
+
11
+ | SQLite | Postgres |
12
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
13
+ | `TEXT` | `TEXT` (unchanged) |
14
+ | `INTEGER` | `BIGINT` |
15
+ | `REAL` | `DOUBLE PRECISION` |
16
+ | `?` placeholders | `$1`, `$2`, … (rewritten by `PostgresAdapter` at runtime — DDL files use `?` only if they're parameterized, which they shouldn't be) |
17
+ | `AUTOINCREMENT` | `GENERATED BY DEFAULT AS IDENTITY` |
18
+ | `INSERT OR REPLACE` | `INSERT … ON CONFLICT (…) DO UPDATE SET …` |
19
+ | Stored `TEXT` with JSON | `JSONB` (always; the dashboard read paths can leverage indexed JSON queries on Postgres) |
20
+
21
+ ## Numbering
22
+
23
+ Files MUST share the numeric prefix of their D1 sibling so the migration runner
24
+ can ensure parity. If `004_logs.sql` exists in `../migrations/`, this directory
25
+ MUST contain `004_logs.sql` as well. A CI check will refuse to merge PRs that
26
+ break parity.
27
+
28
+ ## What's translated
29
+
30
+ The first three migrations are translated as a proof of the pattern. The
31
+ remaining migrations are tracked in
32
+ [`docs/postgres-migration.md`](../../../../docs/postgres-migration.md); they'll
33
+ land alongside the first hosted-Postgres deployment.
34
+
35
+ ## What's NOT translated
36
+
37
+ `009_projects_and_keys.sql` uses SQLite's `BLOB` type for the hashed API key.
38
+ Postgres uses `BYTEA` and the column-name rewrite is in
39
+ `009_projects_and_keys.sql` here (a non-mechanical translation; review carefully
40
+ when porting forward).
@@ -0,0 +1,373 @@
1
+ /**
2
+ * OTLP acceptance suite — exercises the live `/v1/*` endpoints over HTTP
3
+ * against a running collector. Skipped unless `OTLP_ACCEPTANCE_URL` is set.
4
+ *
5
+ * Run via `pnpm run e2e:otlp` from the repo root, which spins up `wrangler
6
+ * dev` on :8790 with `ALLOW_UNAUTHENTICATED=true` and sets the env var for
7
+ * this suite.
8
+ *
9
+ * Covers the three bars the RFC defines for parity with the Go receiver:
10
+ * 1. Stock SDK path — protobuf + gzip + OTLP-JSON all accepted
11
+ * 2. Response envelope — empty on full success, partial_success on over-cap
12
+ * 3. Error contract — 400 on malformed, 415 on bad content-type
13
+ */
14
+
15
+ import {
16
+ create,
17
+ fromBinary,
18
+ fromJson,
19
+ toBinary,
20
+ toJson,
21
+ } from "@bufbuild/protobuf";
22
+ import { describe, expect, it } from "vitest";
23
+
24
+ declare const process: { env: Record<string, string | undefined> };
25
+
26
+ const gzip = async (bytes: Uint8Array): Promise<ArrayBuffer> => {
27
+ const stream = new Response(bytes as BodyInit).body?.pipeThrough(
28
+ new CompressionStream("gzip"),
29
+ );
30
+ return new Response(stream).arrayBuffer();
31
+ };
32
+
33
+ import { ExportLogsServiceRequestSchema } from "./gen/opentelemetry/proto/collector/logs/v1/logs_service_pb.js";
34
+ import { ExportMetricsServiceRequestSchema } from "./gen/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.js";
35
+ import {
36
+ ExportTraceServiceRequestSchema,
37
+ ExportTraceServiceResponseSchema,
38
+ } from "./gen/opentelemetry/proto/collector/trace/v1/trace_service_pb.js";
39
+ import {
40
+ AnyValueSchema,
41
+ KeyValueSchema,
42
+ } from "./gen/opentelemetry/proto/common/v1/common_pb.js";
43
+ import {
44
+ LogRecordSchema,
45
+ ResourceLogsSchema,
46
+ ScopeLogsSchema,
47
+ SeverityNumber,
48
+ } from "./gen/opentelemetry/proto/logs/v1/logs_pb.js";
49
+ import {
50
+ AggregationTemporality,
51
+ GaugeSchema,
52
+ HistogramDataPointSchema,
53
+ HistogramSchema,
54
+ MetricSchema,
55
+ NumberDataPointSchema,
56
+ ResourceMetricsSchema,
57
+ ScopeMetricsSchema,
58
+ SumSchema,
59
+ } from "./gen/opentelemetry/proto/metrics/v1/metrics_pb.js";
60
+ import { ResourceSchema } from "./gen/opentelemetry/proto/resource/v1/resource_pb.js";
61
+ import {
62
+ ResourceSpansSchema,
63
+ ScopeSpansSchema,
64
+ SpanSchema,
65
+ } from "./gen/opentelemetry/proto/trace/v1/trace_pb.js";
66
+
67
+ const base = process.env.OTLP_ACCEPTANCE_URL;
68
+ const suite = base ? describe : describe.skip;
69
+
70
+ const serviceAttr = () =>
71
+ create(KeyValueSchema, {
72
+ key: "service.name",
73
+ value: create(AnyValueSchema, {
74
+ value: { case: "stringValue", value: "e2e-otlp" },
75
+ }),
76
+ });
77
+
78
+ const buildTraces = (count: number) =>
79
+ create(ExportTraceServiceRequestSchema, {
80
+ resourceSpans: [
81
+ create(ResourceSpansSchema, {
82
+ resource: create(ResourceSchema, { attributes: [serviceAttr()] }),
83
+ scopeSpans: [
84
+ create(ScopeSpansSchema, {
85
+ scope: { name: "acceptance" },
86
+ spans: Array.from({ length: count }, (_, i) =>
87
+ create(SpanSchema, {
88
+ traceId: new Uint8Array(16).fill((i % 255) + 1),
89
+ spanId: new Uint8Array(8).fill((i % 255) + 1),
90
+ name: `op-${i}`,
91
+ startTimeUnixNano: 1_700_000_000_000_000_000n + BigInt(i),
92
+ endTimeUnixNano: 1_700_000_000_100_000_000n + BigInt(i),
93
+ }),
94
+ ),
95
+ }),
96
+ ],
97
+ }),
98
+ ],
99
+ });
100
+
101
+ const buildLogs = (message: string) =>
102
+ create(ExportLogsServiceRequestSchema, {
103
+ resourceLogs: [
104
+ create(ResourceLogsSchema, {
105
+ resource: create(ResourceSchema, { attributes: [serviceAttr()] }),
106
+ scopeLogs: [
107
+ create(ScopeLogsSchema, {
108
+ scope: { name: "acceptance" },
109
+ logRecords: [
110
+ create(LogRecordSchema, {
111
+ timeUnixNano: BigInt(Date.now()) * 1_000_000n,
112
+ observedTimeUnixNano: BigInt(Date.now()) * 1_000_000n,
113
+ severityNumber: SeverityNumber.ERROR,
114
+ severityText: "ERROR",
115
+ body: create(AnyValueSchema, {
116
+ value: { case: "stringValue", value: message },
117
+ }),
118
+ }),
119
+ ],
120
+ }),
121
+ ],
122
+ }),
123
+ ],
124
+ });
125
+
126
+ const buildMetrics = () =>
127
+ create(ExportMetricsServiceRequestSchema, {
128
+ resourceMetrics: [
129
+ create(ResourceMetricsSchema, {
130
+ resource: create(ResourceSchema, { attributes: [serviceAttr()] }),
131
+ scopeMetrics: [
132
+ create(ScopeMetricsSchema, {
133
+ scope: { name: "acceptance" },
134
+ metrics: [
135
+ create(MetricSchema, {
136
+ name: "e2e.gauge",
137
+ data: {
138
+ case: "gauge",
139
+ value: create(GaugeSchema, {
140
+ dataPoints: [
141
+ create(NumberDataPointSchema, {
142
+ timeUnixNano: 1_700_000_000_000_000_000n,
143
+ value: { case: "asDouble", value: 42 },
144
+ }),
145
+ ],
146
+ }),
147
+ },
148
+ }),
149
+ create(MetricSchema, {
150
+ name: "e2e.sum",
151
+ data: {
152
+ case: "sum",
153
+ value: create(SumSchema, {
154
+ isMonotonic: true,
155
+ aggregationTemporality: AggregationTemporality.CUMULATIVE,
156
+ dataPoints: [
157
+ create(NumberDataPointSchema, {
158
+ timeUnixNano: 1_700_000_000_000_000_000n,
159
+ value: { case: "asInt", value: 100n },
160
+ }),
161
+ ],
162
+ }),
163
+ },
164
+ }),
165
+ create(MetricSchema, {
166
+ name: "e2e.hist",
167
+ data: {
168
+ case: "histogram",
169
+ value: create(HistogramSchema, {
170
+ aggregationTemporality: AggregationTemporality.DELTA,
171
+ dataPoints: [
172
+ create(HistogramDataPointSchema, {
173
+ timeUnixNano: 1_700_000_000_000_000_000n,
174
+ count: 5n,
175
+ sum: 50,
176
+ explicitBounds: [10, 50],
177
+ bucketCounts: [2n, 2n, 1n],
178
+ }),
179
+ ],
180
+ }),
181
+ },
182
+ }),
183
+ ],
184
+ }),
185
+ ],
186
+ }),
187
+ ],
188
+ });
189
+
190
+ const post = (
191
+ path: string,
192
+ body: BodyInit | Uint8Array,
193
+ headers: Record<string, string> = {},
194
+ ) =>
195
+ fetch(`${base}${path}`, {
196
+ method: "POST",
197
+ headers: { "Content-Type": "application/json", ...headers },
198
+ body: body as BodyInit,
199
+ });
200
+
201
+ suite("OTLP acceptance (live)", () => {
202
+ it("health check is reachable", async () => {
203
+ const res = await fetch(`${base}/health`);
204
+ expect(res.ok).toBe(true);
205
+ });
206
+
207
+ describe("/v1/traces", () => {
208
+ it("accepts JSON and returns empty envelope (200)", async () => {
209
+ const msg = buildTraces(3);
210
+ const body = JSON.stringify(toJson(ExportTraceServiceRequestSchema, msg));
211
+ const res = await post("/v1/traces", body);
212
+ expect(res.status).toBe(200);
213
+ expect(res.headers.get("content-type")).toContain("application/json");
214
+ expect(await res.json()).toEqual({});
215
+ });
216
+
217
+ it("accepts protobuf and returns empty envelope (200)", async () => {
218
+ const bytes = toBinary(ExportTraceServiceRequestSchema, buildTraces(3));
219
+ const res = await post("/v1/traces", bytes, {
220
+ "Content-Type": "application/x-protobuf",
221
+ });
222
+ expect(res.status).toBe(200);
223
+ expect(res.headers.get("content-type")).toContain(
224
+ "application/x-protobuf",
225
+ );
226
+ const msg = fromBinary(
227
+ ExportTraceServiceResponseSchema,
228
+ new Uint8Array(await res.arrayBuffer()),
229
+ );
230
+ expect(msg.partialSuccess).toBeUndefined();
231
+ });
232
+
233
+ it("decompresses gzip request bodies", async () => {
234
+ const raw = new TextEncoder().encode(
235
+ JSON.stringify(toJson(ExportTraceServiceRequestSchema, buildTraces(2))),
236
+ );
237
+ const res = await post("/v1/traces", await gzip(raw), {
238
+ "Content-Encoding": "gzip",
239
+ });
240
+ expect(res.status).toBe(200);
241
+ });
242
+
243
+ it("returns partial_success when batch exceeds the cap", async () => {
244
+ const bytes = toBinary(ExportTraceServiceRequestSchema, buildTraces(501));
245
+ const res = await post("/v1/traces", bytes, {
246
+ "Content-Type": "application/x-protobuf",
247
+ });
248
+ expect(res.status).toBe(200);
249
+ const msg = fromBinary(
250
+ ExportTraceServiceResponseSchema,
251
+ new Uint8Array(await res.arrayBuffer()),
252
+ );
253
+ expect(msg.partialSuccess?.rejectedSpans).toBe(1n);
254
+ expect(msg.partialSuccess?.errorMessage).toMatch(/cap/);
255
+ });
256
+
257
+ it("accepts valid spans and rejects malformed ones in the same batch (RFC bar #3)", async () => {
258
+ // 99 valid spans + 1 with an invalid (too-short) trace id.
259
+ // The Go reference receiver accepts the 99 and reports 1 rejected
260
+ // via partial_success.
261
+ const msg = buildTraces(99);
262
+ const bad = msg.resourceSpans[0]?.scopeSpans[0]?.spans[0];
263
+ expect(bad).toBeDefined();
264
+ if (!bad) return;
265
+ const badClone = { ...bad, traceId: new Uint8Array([1, 2, 3]) };
266
+ msg.resourceSpans[0]?.scopeSpans[0]?.spans.push(badClone);
267
+
268
+ // protobuf refuses to encode invalid trace_id bytes, so send JSON.
269
+ const json = toJson(ExportTraceServiceRequestSchema, msg) as {
270
+ resourceSpans: Array<{
271
+ scopeSpans: Array<{ spans: Array<{ traceId: string }> }>;
272
+ }>;
273
+ };
274
+ // Corrupt the last span's traceId after JSON encoding so it reaches
275
+ // the receiver but fails `normalizeHex` length validation.
276
+ const spans = json.resourceSpans[0]?.scopeSpans[0]?.spans;
277
+ const lastSpan = spans?.[spans.length - 1];
278
+ expect(lastSpan).toBeDefined();
279
+ if (!lastSpan) return;
280
+ lastSpan.traceId = "010203"; // too short for hex or base64
281
+
282
+ const res = await post("/v1/traces", JSON.stringify(json));
283
+ expect(res.status).toBe(200);
284
+ const body = (await res.json()) as {
285
+ partialSuccess?: {
286
+ rejectedSpans?: string;
287
+ errorMessage?: string;
288
+ };
289
+ };
290
+ expect(body.partialSuccess?.rejectedSpans).toBe("1");
291
+ expect(body.partialSuccess?.errorMessage).toMatch(/invalid|malformed/i);
292
+ });
293
+
294
+ it("rejects malformed JSON with 400", async () => {
295
+ const res = await post("/v1/traces", "{not json");
296
+ expect(res.status).toBe(400);
297
+ });
298
+
299
+ it("rejects unknown content-type with 415", async () => {
300
+ const res = await post("/v1/traces", "whatever", {
301
+ "Content-Type": "text/plain",
302
+ });
303
+ expect(res.status).toBe(415);
304
+ });
305
+
306
+ it("rejects unknown content-encoding with 415", async () => {
307
+ const res = await post(
308
+ "/v1/traces",
309
+ JSON.stringify(toJson(ExportTraceServiceRequestSchema, buildTraces(1))),
310
+ { "Content-Encoding": "brotli" },
311
+ );
312
+ expect(res.status).toBe(415);
313
+ });
314
+ });
315
+
316
+ describe("/v1/logs", () => {
317
+ it("accepts OTLP JSON logs (empty envelope on full success)", async () => {
318
+ const body = JSON.stringify(
319
+ toJson(ExportLogsServiceRequestSchema, buildLogs("json-log")),
320
+ );
321
+ const res = await post("/v1/logs", body);
322
+ expect(res.status).toBe(200);
323
+ expect(await res.json()).toEqual({});
324
+ });
325
+
326
+ it("accepts OTLP protobuf logs", async () => {
327
+ const bytes = toBinary(
328
+ ExportLogsServiceRequestSchema,
329
+ buildLogs("protobuf-log"),
330
+ );
331
+ const res = await post("/v1/logs", bytes, {
332
+ "Content-Type": "application/x-protobuf",
333
+ });
334
+ expect(res.status).toBe(200);
335
+ });
336
+ });
337
+
338
+ describe("/v1/metrics", () => {
339
+ it("accepts OTLP JSON gauge + sum + histogram", async () => {
340
+ const body = JSON.stringify(
341
+ toJson(ExportMetricsServiceRequestSchema, buildMetrics()),
342
+ );
343
+ const res = await post("/v1/metrics", body);
344
+ expect(res.status).toBe(200);
345
+ expect(await res.json()).toEqual({});
346
+ });
347
+
348
+ it("accepts OTLP protobuf metrics", async () => {
349
+ const bytes = toBinary(ExportMetricsServiceRequestSchema, buildMetrics());
350
+ const res = await post("/v1/metrics", bytes, {
351
+ "Content-Type": "application/x-protobuf",
352
+ });
353
+ expect(res.status).toBe(200);
354
+ });
355
+
356
+ it("round-trips proto-JSON → collector → decode without loss", async () => {
357
+ const json = toJson(ExportMetricsServiceRequestSchema, buildMetrics());
358
+ // Round-trip sanity: re-parse the JSON we're about to POST and confirm
359
+ // it's still schema-valid. Catches proto-JSON encoding regressions.
360
+ expect(() =>
361
+ fromJson(ExportMetricsServiceRequestSchema, json),
362
+ ).not.toThrow();
363
+ });
364
+ });
365
+ });
366
+
367
+ if (!base) {
368
+ // Emit a visible note even if the suite is skipped, so CI doesn't silently
369
+ // pass without having run the acceptance tests.
370
+ console.warn(
371
+ "[otlp-acceptance] OTLP_ACCEPTANCE_URL not set — skipping live suite",
372
+ );
373
+ }
@@ -0,0 +1,127 @@
1
+ import type { JsonValue } from "@bufbuild/protobuf";
2
+ import type { Context } from "hono";
3
+
4
+ export type OtlpWireFormat = "json" | "protobuf";
5
+
6
+ export interface ReadBodyResult {
7
+ bytes: Uint8Array;
8
+ wireFormat: OtlpWireFormat;
9
+ }
10
+
11
+ export class OtlpDecodeError extends Error {
12
+ constructor(
13
+ message: string,
14
+ public readonly status: 400 | 415,
15
+ ) {
16
+ super(message);
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Reads and decompresses an OTLP request body, returning the raw bytes plus
22
+ * the inferred wire format. Throws `OtlpDecodeError` for unsupported content-
23
+ * types or malformed gzip.
24
+ */
25
+ export const readOtlpBody = async (c: Context): Promise<ReadBodyResult> => {
26
+ const contentType = (c.req.header("content-type") ?? "").toLowerCase();
27
+ const wireFormat = detectWireFormat(contentType);
28
+
29
+ let body = await c.req.arrayBuffer();
30
+ const encoding = (c.req.header("content-encoding") ?? "").toLowerCase();
31
+ if (encoding === "gzip") {
32
+ body = await gunzip(body);
33
+ } else if (encoding && encoding !== "identity") {
34
+ throw new OtlpDecodeError(`Unsupported content-encoding: ${encoding}`, 415);
35
+ }
36
+
37
+ return { bytes: new Uint8Array(body), wireFormat };
38
+ };
39
+
40
+ const detectWireFormat = (contentType: string): OtlpWireFormat => {
41
+ if (contentType.includes("application/x-protobuf")) return "protobuf";
42
+ if (contentType.includes("application/json")) return "json";
43
+ throw new OtlpDecodeError(
44
+ `Unsupported content-type: ${contentType || "(missing)"}`,
45
+ 415,
46
+ );
47
+ };
48
+
49
+ const gunzip = async (input: ArrayBuffer): Promise<ArrayBuffer> => {
50
+ const stream = new Response(input).body?.pipeThrough(
51
+ new DecompressionStream("gzip"),
52
+ );
53
+ if (!stream) throw new OtlpDecodeError("Empty gzip body", 400);
54
+ return new Response(stream).arrayBuffer();
55
+ };
56
+
57
+ export const decodeJsonBody = (bytes: Uint8Array): JsonValue => {
58
+ const text = new TextDecoder().decode(bytes);
59
+ if (!text.length) return {};
60
+ const parsed = JSON.parse(text) as JsonValue;
61
+ rewriteHexIdsToBase64(parsed);
62
+ return parsed;
63
+ };
64
+
65
+ /**
66
+ * The OTLP JSON encoding spec requires `trace_id` (16 bytes) and `span_id`
67
+ * (8 bytes) to be lowercase hex strings — but proto-JSON's default encoding
68
+ * for `bytes` fields is base64, which is what protobuf-es's `fromJson`
69
+ * expects. The Go reference receiver accepts both; we do too, by walking
70
+ * the parsed JSON and converting any hex-shaped ID field to base64 before
71
+ * handing it to the schema decoder.
72
+ *
73
+ * Mutates the input in-place. Recognized field names cover every place a
74
+ * trace/span ID appears in OTLP: span, link, log record, exemplar.
75
+ */
76
+ const rewriteHexIdsToBase64 = (node: JsonValue): void => {
77
+ if (Array.isArray(node)) {
78
+ for (const item of node) rewriteHexIdsToBase64(item);
79
+ return;
80
+ }
81
+ if (!node || typeof node !== "object") return;
82
+ const obj = node as Record<string, JsonValue>;
83
+ for (const key of Object.keys(obj)) {
84
+ const value = obj[key];
85
+ if (typeof value === "string") {
86
+ if ((key === "traceId" || key === "trace_id") && isHex(value, 32)) {
87
+ obj[key] = hexToBase64(value);
88
+ } else if (
89
+ (key === "spanId" ||
90
+ key === "span_id" ||
91
+ key === "parentSpanId" ||
92
+ key === "parent_span_id") &&
93
+ isHex(value, 16)
94
+ ) {
95
+ obj[key] = hexToBase64(value);
96
+ } else if (isTimestampKey(key) && !isUint64String(value)) {
97
+ delete obj[key];
98
+ }
99
+ } else if (isTimestampKey(key) && typeof value !== "number") {
100
+ delete obj[key];
101
+ } else {
102
+ rewriteHexIdsToBase64(value);
103
+ }
104
+ }
105
+ };
106
+
107
+ const isHex = (s: string, length: number): boolean =>
108
+ s.length === length && /^[0-9a-f]+$/i.test(s);
109
+
110
+ const isTimestampKey = (key: string): boolean =>
111
+ key === "timeUnixNano" ||
112
+ key === "observedTimeUnixNano" ||
113
+ key === "startTimeUnixNano" ||
114
+ key === "endTimeUnixNano";
115
+
116
+ const isUint64String = (s: string): boolean => /^\d+$/.test(s);
117
+
118
+ const hexToBase64 = (hex: string): string => {
119
+ const bytes = new Uint8Array(hex.length / 2);
120
+ for (let i = 0; i < bytes.length; i++) {
121
+ bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
122
+ }
123
+ let binary = "";
124
+ for (let i = 0; i < bytes.length; i++)
125
+ binary += String.fromCharCode(bytes[i] ?? 0);
126
+ return btoa(binary);
127
+ };