@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,160 @@
1
+ import type { LogRecord, LogSeverity } from "@obsunified/types";
2
+ import { getConfiguredRetentionHours } from "@obsunified/types/constants";
3
+ import type { CollectorPlugin } from "../framework/collector";
4
+ import { LogsStore } from "../lib/logs-store";
5
+ import { sqlDbFor } from "../lib/sql-db";
6
+ import { logToTailEvent, publishTail } from "../lib/tail-publisher";
7
+ import {
8
+ type DecodedLogRecord,
9
+ decodeLogsRequest,
10
+ OtlpDecodeError,
11
+ type ReadBodyResult,
12
+ readOtlpBody,
13
+ } from "../otlp/decode";
14
+ import { logsResponse, otlpRetryableError } from "../otlp/response";
15
+ import { getProjectId } from "./_context";
16
+
17
+ const MAX_LOGS_PER_REQUEST = 1000;
18
+
19
+ export const logsReceiverPlugin: CollectorPlugin = {
20
+ name: "logs-receiver",
21
+ register(app, runtime) {
22
+ app.post("/v1/logs", async (c) => {
23
+ const projectId = getProjectId(c);
24
+
25
+ let body: ReadBodyResult;
26
+ try {
27
+ body = await readOtlpBody(c);
28
+ } catch (err) {
29
+ if (err instanceof OtlpDecodeError) {
30
+ return c.json({ error: err.message }, err.status);
31
+ }
32
+ throw err;
33
+ }
34
+
35
+ let decoded: DecodedLogRecord[];
36
+ try {
37
+ decoded = decodeLogsRequest(body);
38
+ } catch (err) {
39
+ if (err instanceof OtlpDecodeError) {
40
+ return c.json({ error: err.message }, err.status);
41
+ }
42
+ throw err;
43
+ }
44
+
45
+ let rejected = 0;
46
+ if (decoded.length > MAX_LOGS_PER_REQUEST) {
47
+ rejected = decoded.length - MAX_LOGS_PER_REQUEST;
48
+ decoded = decoded.slice(0, MAX_LOGS_PER_REQUEST);
49
+ }
50
+
51
+ const store = new LogsStore(sqlDbFor(c.env));
52
+ const retentionHours = getConfiguredRetentionHours(c.env.RETENTION_HOURS);
53
+ const now = new Date();
54
+ const nowStr = now.toISOString();
55
+ const expires = new Date(
56
+ now.getTime() + retentionHours * 60 * 60 * 1000,
57
+ ).toISOString();
58
+
59
+ const records: LogRecord[] = decoded.map((d) => {
60
+ const sessionId =
61
+ d.attributes &&
62
+ typeof d.attributes["session.id"] === "string" &&
63
+ (d.attributes["session.id"] as string).length > 0
64
+ ? (d.attributes["session.id"] as string)
65
+ : null;
66
+ // RFC 0004 — denormalize obs.interaction.id from log
67
+ // attributes when the producer stamped it (telemetry-sdk's
68
+ // logger inherits the active span's attribute).
69
+ const interactionId =
70
+ d.attributes &&
71
+ typeof d.attributes["obs.interaction.id"] === "string" &&
72
+ (d.attributes["obs.interaction.id"] as string).length > 0
73
+ ? (d.attributes["obs.interaction.id"] as string)
74
+ : null;
75
+ return {
76
+ projectId,
77
+ logId: crypto.randomUUID(),
78
+ traceId: d.traceId,
79
+ spanId: d.spanId,
80
+ serviceName: d.serviceName,
81
+ severity: d.severity,
82
+ severityNumber: d.severityNumber || severityToNumber(d.severity),
83
+ loggerName: d.loggerName,
84
+ message: d.message,
85
+ attributesJson: d.attributes ? JSON.stringify(d.attributes) : null,
86
+ flags: d.flags,
87
+ droppedAttributesCount: d.droppedAttributesCount,
88
+ occurredAt: d.occurredAt,
89
+ receivedAt: nowStr,
90
+ expiresAt: expires,
91
+ sessionId,
92
+ interactionId,
93
+ };
94
+ });
95
+
96
+ try {
97
+ await runtime.withChildSpan("logs.ingest", async (span) => {
98
+ span.setAttribute("logs.records_received", decoded.length + rejected);
99
+ span.setAttribute("logs.records_rejected", rejected);
100
+ span.setAttribute("logs.records_inserted", records.length);
101
+ span.setAttribute("project.id", projectId);
102
+ await store.ingestBatch(records);
103
+ });
104
+ } catch (err) {
105
+ runtime.logger.error("[/v1/logs] storage error", {
106
+ project_id: projectId,
107
+ error: err instanceof Error ? err.message : String(err),
108
+ });
109
+ return otlpRetryableError(c, 503, "Storage temporarily unavailable");
110
+ }
111
+
112
+ if (records.length > 0 && c.env.TAIL_HUB) {
113
+ const events = records.map(logToTailEvent);
114
+ c.executionCtx.waitUntil(publishTail(c.env, events));
115
+ }
116
+
117
+ if (rejected > 0) {
118
+ return logsResponse(c, body.wireFormat, {
119
+ rejected,
120
+ errorMessage: `Batch exceeded ${MAX_LOGS_PER_REQUEST}-record cap; ${rejected} record(s) dropped`,
121
+ });
122
+ }
123
+ return logsResponse(c, body.wireFormat);
124
+ });
125
+
126
+ app.get("/internal/logs/overview", async (c) => {
127
+ const projectId = getProjectId(c);
128
+ const store = new LogsStore(sqlDbFor(c.env));
129
+ const query = c.req.query();
130
+ const hours = parseInt(query.hours || "24", 10);
131
+
132
+ const response = await store.getLogs({
133
+ projectId,
134
+ hours,
135
+ service: query.service,
136
+ severity: query.severity as LogSeverity | undefined,
137
+ traceId: query.traceId,
138
+ limit: parseInt(query.limit || "100", 10),
139
+ search: query.search,
140
+ });
141
+
142
+ return c.json(response);
143
+ });
144
+ },
145
+ };
146
+
147
+ const severityToNumber = (s: LogSeverity): number => {
148
+ switch (s) {
149
+ case "DEBUG":
150
+ return 5;
151
+ case "INFO":
152
+ return 9;
153
+ case "WARN":
154
+ return 13;
155
+ case "ERROR":
156
+ return 17;
157
+ case "FATAL":
158
+ return 21;
159
+ }
160
+ };
@@ -0,0 +1,91 @@
1
+ /** OTLP/HTTP metrics receiver. Accepts JSON or protobuf, with gzip. */
2
+
3
+ import { getConfiguredRetentionHours } from "@obsunified/types/constants";
4
+ import type { CollectorPlugin } from "../framework/collector";
5
+ import { MetricsStore } from "../lib/metrics-store";
6
+ import { sqlDbFor } from "../lib/sql-db";
7
+ import {
8
+ decodeMetricsRequest,
9
+ OtlpDecodeError,
10
+ type ReadBodyResult,
11
+ readOtlpBody,
12
+ } from "../otlp/decode";
13
+ import { metricsResponse, otlpRetryableError } from "../otlp/response";
14
+ import { getProjectId } from "./_context";
15
+
16
+ const MAX_POINTS_PER_REQUEST = 2000;
17
+
18
+ export const metricsReceiverPlugin: CollectorPlugin = {
19
+ name: "metrics-http-receiver",
20
+ register(app, runtime) {
21
+ app.post("/v1/metrics", async (c) => {
22
+ const projectId = getProjectId(c);
23
+
24
+ let body: ReadBodyResult;
25
+ try {
26
+ body = await readOtlpBody(c);
27
+ } catch (err) {
28
+ if (err instanceof OtlpDecodeError) {
29
+ return c.json({ error: err.message }, err.status);
30
+ }
31
+ throw err;
32
+ }
33
+
34
+ let points: ReturnType<typeof decodeMetricsRequest>;
35
+ try {
36
+ points = decodeMetricsRequest(body);
37
+ } catch (err) {
38
+ if (err instanceof OtlpDecodeError) {
39
+ return c.json({ error: err.message }, err.status);
40
+ }
41
+ throw err;
42
+ }
43
+
44
+ let rejected = 0;
45
+ if (points.length > MAX_POINTS_PER_REQUEST) {
46
+ rejected = points.length - MAX_POINTS_PER_REQUEST;
47
+ points = points.slice(0, MAX_POINTS_PER_REQUEST);
48
+ }
49
+
50
+ const retentionHours = getConfiguredRetentionHours(c.env.RETENTION_HOURS);
51
+ const now = new Date();
52
+ const receivedAt = now.toISOString();
53
+ const expiresAt = new Date(
54
+ now.getTime() + retentionHours * 60 * 60 * 1000,
55
+ ).toISOString();
56
+
57
+ const store = new MetricsStore(sqlDbFor(c.env));
58
+ try {
59
+ await runtime.withChildSpan("metrics.ingest", async (span) => {
60
+ span.setAttribute(
61
+ "metrics.points_received",
62
+ points.length + rejected,
63
+ );
64
+ span.setAttribute("metrics.points_rejected", rejected);
65
+ span.setAttribute("metrics.points_inserted", points.length);
66
+ span.setAttribute("project.id", projectId);
67
+ await store.ingestBatch({
68
+ projectId,
69
+ points,
70
+ receivedAt,
71
+ expiresAt,
72
+ });
73
+ });
74
+ } catch (err) {
75
+ runtime.logger.error("[/v1/metrics] storage error", {
76
+ project_id: projectId,
77
+ error: err instanceof Error ? err.message : String(err),
78
+ });
79
+ return otlpRetryableError(c, 503, "Storage temporarily unavailable");
80
+ }
81
+
82
+ if (rejected > 0) {
83
+ return metricsResponse(c, body.wireFormat, {
84
+ rejected,
85
+ errorMessage: `Batch exceeded ${MAX_POINTS_PER_REQUEST}-point cap; ${rejected} point(s) dropped`,
86
+ });
87
+ }
88
+ return metricsResponse(c, body.wireFormat);
89
+ });
90
+ },
91
+ };
@@ -0,0 +1,54 @@
1
+ import type { CollectorPlugin } from "../framework/collector";
2
+ import { sqlDbFor } from "../lib/sql-db";
3
+
4
+ /**
5
+ * Drives the in-dashboard Onboarding tab — returns counts of signal
6
+ * types, used to decide which "you need to wire X" snippets to show
7
+ * the user.
8
+ *
9
+ * Returns zeros for tables that don't yet exist (e.g. fresh DB) rather
10
+ * than 500ing — onboarding has to work on day zero.
11
+ */
12
+ export const onboardingRoutesPlugin: CollectorPlugin = {
13
+ name: "onboarding-routes",
14
+ register(app) {
15
+ app.get("/internal/onboarding/counts", async (c) => {
16
+ const db = sqlDbFor(c.env);
17
+ const counts = await Promise.all([
18
+ safeCount(db, "SELECT COUNT(*) AS n FROM telemetry_spans"),
19
+ safeCount(db, "SELECT COUNT(*) AS n FROM logs"),
20
+ safeCount(db, "SELECT COUNT(*) AS n FROM usage_events"),
21
+ safeCount(db, "SELECT COUNT(*) AS n FROM user_profiles"),
22
+ safeCount(db, "SELECT COUNT(*) AS n FROM ai_calls"),
23
+ safeCount(db, "SELECT COUNT(*) AS n FROM session_replay_metadata"),
24
+ safeCount(
25
+ db,
26
+ "SELECT COUNT(*) AS n FROM telemetry_spans WHERE interaction_id IS NOT NULL",
27
+ ),
28
+ ]);
29
+
30
+ return c.json({
31
+ spans: counts[0],
32
+ logs: counts[1],
33
+ usageEvents: counts[2],
34
+ identifiedUsers: counts[3],
35
+ aiCalls: counts[4],
36
+ replayChunks: counts[5],
37
+ spansWithInteraction: counts[6],
38
+ });
39
+ });
40
+ },
41
+ };
42
+
43
+ async function safeCount(
44
+ db: { prepare: (sql: string) => { first: <T>() => Promise<T | null> } },
45
+ sql: string,
46
+ ): Promise<number> {
47
+ try {
48
+ const row = await db.prepare(sql).first<{ n: number }>();
49
+ return Number(row?.n ?? 0);
50
+ } catch {
51
+ // Table doesn't exist yet — fresh DB, no signals of this type.
52
+ return 0;
53
+ }
54
+ }
@@ -0,0 +1,124 @@
1
+ /** OTLP/HTTP trace receiver. Accepts JSON or protobuf, with gzip. */
2
+
3
+ import { getConfiguredRetentionHours } from "@obsunified/types/constants";
4
+ import type { CollectorPlugin } from "../framework/collector";
5
+ import { toStoredSpans } from "../lib/otlp";
6
+ import { publishTail, spanToTailEvent } from "../lib/tail-publisher";
7
+ import {
8
+ decodeTraceRequest,
9
+ OtlpDecodeError,
10
+ type ReadBodyResult,
11
+ readOtlpBody,
12
+ } from "../otlp/decode";
13
+ import { otlpRetryableError, traceResponse } from "../otlp/response";
14
+ import { getProjectId } from "./_context";
15
+
16
+ const MAX_SPANS_PER_REQUEST = 500;
17
+
18
+ export const otlpReceiverPlugin: CollectorPlugin = {
19
+ name: "otlp-http-receiver",
20
+ register(app, runtime) {
21
+ app.post("/v1/traces", async (c) => {
22
+ const projectId = getProjectId(c);
23
+ const routeContext = runtime.createRouteContext(c.env, c);
24
+
25
+ let body: ReadBodyResult;
26
+ try {
27
+ body = await readOtlpBody(c);
28
+ } catch (err) {
29
+ if (err instanceof OtlpDecodeError) {
30
+ return c.json({ error: err.message }, err.status);
31
+ }
32
+ throw err;
33
+ }
34
+
35
+ let payload: ReturnType<typeof decodeTraceRequest>;
36
+ try {
37
+ payload = decodeTraceRequest(body);
38
+ } catch (err) {
39
+ if (err instanceof OtlpDecodeError) {
40
+ return c.json({ error: err.message }, err.status);
41
+ }
42
+ throw err;
43
+ }
44
+
45
+ // Count spans and truncate to cap, tracking rejected excess for the
46
+ // partial_success envelope. We rebuild a trimmed payload rather than
47
+ // touching the rest of the pipeline.
48
+ let total = 0;
49
+ let rejected = 0;
50
+ const trimmedResourceSpans = [];
51
+ for (const rs of payload.resourceSpans ?? []) {
52
+ const scopeSpans = [];
53
+ for (const ss of rs.scopeSpans ?? []) {
54
+ const spans = [];
55
+ for (const s of ss.spans ?? []) {
56
+ if (total >= MAX_SPANS_PER_REQUEST) {
57
+ rejected++;
58
+ } else {
59
+ spans.push(s);
60
+ total++;
61
+ }
62
+ }
63
+ scopeSpans.push({ ...ss, spans });
64
+ }
65
+ trimmedResourceSpans.push({ ...rs, scopeSpans });
66
+ }
67
+
68
+ const parsedSpans = toStoredSpans(
69
+ { resourceSpans: trimmedResourceSpans },
70
+ projectId,
71
+ routeContext.now,
72
+ getConfiguredRetentionHours(c.env.RETENTION_HOURS),
73
+ );
74
+ // `toStoredSpans` silently drops spans with invalid trace/span IDs
75
+ // (length mismatch, non-hex). Surface those to the caller via
76
+ // partial_success rather than making them disappear.
77
+ const malformed = total - parsedSpans.length;
78
+ const totalRejected = rejected + malformed;
79
+
80
+ const spans = await runtime.runSpanProcessors(parsedSpans, routeContext);
81
+ const store = runtime.createStore(c.env);
82
+ try {
83
+ await runtime.withChildSpan("traces.ingest", async (span) => {
84
+ span.setAttribute("traces.spans_received", total);
85
+ span.setAttribute("traces.spans_rejected", totalRejected);
86
+ span.setAttribute("traces.spans_inserted", spans.length);
87
+ span.setAttribute("project.id", projectId);
88
+ const result = await store.ingest(spans);
89
+ // Distinct traces touched by this batch — the write-side
90
+ // denominator for the gap/trace-detail read-rate ratio (Q3).
91
+ span.setAttribute("traces.trace_count", result.traceCount);
92
+ });
93
+ } catch (err) {
94
+ runtime.logger.error("[/v1/traces] storage error", {
95
+ project_id: projectId,
96
+ error: err instanceof Error ? err.message : String(err),
97
+ });
98
+ return otlpRetryableError(c, 503, "Storage temporarily unavailable");
99
+ }
100
+
101
+ if (spans.length > 0 && c.env.TAIL_HUB) {
102
+ const events = spans.map(spanToTailEvent);
103
+ c.executionCtx.waitUntil(publishTail(c.env, events));
104
+ }
105
+
106
+ if (totalRejected > 0) {
107
+ const parts: string[] = [];
108
+ if (rejected > 0)
109
+ parts.push(
110
+ `${rejected} span(s) dropped over ${MAX_SPANS_PER_REQUEST}-span cap`,
111
+ );
112
+ if (malformed > 0)
113
+ parts.push(
114
+ `${malformed} span(s) rejected (invalid trace_id or span_id)`,
115
+ );
116
+ return traceResponse(c, body.wireFormat, {
117
+ rejected: totalRejected,
118
+ errorMessage: parts.join("; "),
119
+ });
120
+ }
121
+ return traceResponse(c, body.wireFormat);
122
+ });
123
+ },
124
+ };
@@ -0,0 +1,165 @@
1
+ import type { CollectorPlugin } from "../framework/collector";
2
+ import { MetricsStore } from "../lib/metrics-store";
3
+ import { dialectFor, sqlDbFor } from "../lib/sql-db";
4
+ import { getProjectId } from "./_context";
5
+
6
+ export const platformRoutesPlugin: CollectorPlugin = {
7
+ name: "platform-routes",
8
+ register(app) {
9
+ app.get("/internal/platform/resources", async (c) => {
10
+ const projectId = getProjectId(c);
11
+ const db = sqlDbFor(c.env);
12
+
13
+ const countFor = async (table: string) => {
14
+ const row = await db
15
+ .prepare(`SELECT COUNT(*) as c FROM ${table} WHERE project_id = ?`)
16
+ .bind(projectId)
17
+ .first<{ c: number }>();
18
+ return row?.c ?? 0;
19
+ };
20
+
21
+ const eventsCount = await countFor("usage_events");
22
+ const tracesCount = await countFor("telemetry_spans");
23
+ const logsCount = await countFor("logs");
24
+ const aiCallsCount = await countFor("ai_calls");
25
+
26
+ const d1RowDensity = eventsCount + tracesCount + logsCount + aiCallsCount;
27
+
28
+ let r2StorageBytes = 0;
29
+ try {
30
+ const r2Info = await db
31
+ .prepare(
32
+ "SELECT SUM(storage_bytes) as s FROM session_replay_metadata WHERE project_id = ?",
33
+ )
34
+ .bind(projectId)
35
+ .first<{ s: number }>();
36
+ r2StorageBytes = r2Info?.s ?? 0;
37
+ } catch {
38
+ r2StorageBytes = 0;
39
+ }
40
+
41
+ return c.json({
42
+ success: true,
43
+ resources: {
44
+ d1: {
45
+ rowDensity: d1RowDensity,
46
+ eventsCount,
47
+ tracesCount,
48
+ logsCount,
49
+ aiCallsCount,
50
+ },
51
+ r2: {
52
+ storageBytes: r2StorageBytes,
53
+ },
54
+ worker: {
55
+ cpuMs: 0,
56
+ memoryBytes: 0,
57
+ requestsCount: 0,
58
+ status: "Needs Cloudflare Auth Token for live metrics",
59
+ },
60
+ },
61
+ });
62
+ });
63
+
64
+ // RFC 0009 Phase 5.2 — Linux hosts mode. Queries the latest
65
+ // gauge points for the standard OTel host-metrics semconv
66
+ // (system.cpu.utilization, system.memory.usage, etc.) grouped
67
+ // by host.name. Returns an empty array if no `system.*` series
68
+ // exist; the dashboard renders Cloudflare-only mode in that case.
69
+ app.get("/internal/platform/hosts", async (c) => {
70
+ const projectId = getProjectId(c);
71
+ const db = sqlDbFor(c.env);
72
+ const dialect = dialectFor(db);
73
+
74
+ const rows = await db
75
+ .prepare(
76
+ `WITH host_series AS (
77
+ SELECT
78
+ s.id AS series_id,
79
+ s.name AS metric_name,
80
+ ${dialect.jsonText("s.resource_attrs_json", "$.host\\u002Ename")} AS host_name,
81
+ s.attributes_json
82
+ FROM metric_series s
83
+ WHERE s.project_id = ?
84
+ AND s.name LIKE 'system.%'
85
+ ),
86
+ latest_points AS (
87
+ SELECT
88
+ p.series_id,
89
+ p.value,
90
+ p.received_at,
91
+ ROW_NUMBER() OVER (PARTITION BY p.series_id ORDER BY p.received_at DESC) AS rn
92
+ FROM metric_point p
93
+ WHERE p.project_id = ?
94
+ AND p.received_at >= ${dialect.sinceMinutes("15")}
95
+ AND p.series_id IN (SELECT series_id FROM host_series)
96
+ )
97
+ SELECT
98
+ hs.host_name,
99
+ hs.metric_name,
100
+ lp.value,
101
+ lp.received_at
102
+ FROM latest_points lp
103
+ JOIN host_series hs ON hs.series_id = lp.series_id
104
+ WHERE lp.rn = 1
105
+ ORDER BY hs.host_name, hs.metric_name`,
106
+ )
107
+ .bind(projectId, projectId)
108
+ .all<{
109
+ host_name: string | null;
110
+ metric_name: string;
111
+ value: number | null;
112
+ received_at: string;
113
+ }>();
114
+
115
+ // Bucket per host — null host_name means resource_attrs_json
116
+ // didn't carry host.name, group as <unknown>.
117
+ const byHost = new Map<
118
+ string,
119
+ { host: string; metrics: Record<string, number>; updatedAt: string }
120
+ >();
121
+ for (const row of rows.results) {
122
+ const host = row.host_name ?? "<unknown>";
123
+ if (!byHost.has(host)) {
124
+ byHost.set(host, {
125
+ host,
126
+ metrics: {},
127
+ updatedAt: row.received_at,
128
+ });
129
+ }
130
+ const entry = byHost.get(host);
131
+ if (!entry) continue;
132
+ if (row.value !== null) entry.metrics[row.metric_name] = row.value;
133
+ if (row.received_at > entry.updatedAt)
134
+ entry.updatedAt = row.received_at;
135
+ }
136
+
137
+ return c.json({
138
+ success: true,
139
+ hosts: Array.from(byHost.values()),
140
+ });
141
+ });
142
+
143
+ app.get("/internal/platform/metric-exemplars", async (c) => {
144
+ const projectId = getProjectId(c);
145
+ const limit = Math.max(
146
+ 1,
147
+ Math.min(Number.parseInt(c.req.query("limit") ?? "20", 10) || 20, 100),
148
+ );
149
+ const metricPrefix = c.req.query("metric_prefix") ?? "system.";
150
+ const serviceName = c.req.query("service") ?? null;
151
+ const store = new MetricsStore(sqlDbFor(c.env));
152
+ const exemplars = await store.recentExemplars({
153
+ projectId,
154
+ serviceName,
155
+ metricPrefix,
156
+ limit,
157
+ });
158
+ return c.json({
159
+ success: true,
160
+ exemplars,
161
+ timestamp: new Date().toISOString(),
162
+ });
163
+ });
164
+ },
165
+ };