@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.
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +9 -0
- package/dist/auth.js.map +1 -0
- package/dist/chunk-UGWDOGGH.js +319 -0
- package/dist/chunk-UGWDOGGH.js.map +1 -0
- package/dist/chunk-XKQTVIRE.js +671 -0
- package/dist/chunk-XKQTVIRE.js.map +1 -0
- package/dist/index.d.ts +1197 -0
- package/dist/index.js +17707 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest-auth-HFVO1zIp.d.ts +165 -0
- package/dist/openai-FJYOQ7WO.js +384 -0
- package/dist/openai-FJYOQ7WO.js.map +1 -0
- package/package.json +73 -0
- package/src/ai-parity/ai-parity.acceptance.test.ts +449 -0
- package/src/analyses/derive.ts +642 -0
- package/src/analyses/index.ts +50 -0
- package/src/analyses/investigations.ts +277 -0
- package/src/analyses/tier0.ts +568 -0
- package/src/auth/dashboard-auth.ts +269 -0
- package/src/auth/ingest-auth.ts +147 -0
- package/src/durable-objects/tail-hub.test.ts +32 -0
- package/src/durable-objects/tail-hub.ts +158 -0
- package/src/framework/collector.ts +467 -0
- package/src/framework/env.ts +90 -0
- package/src/framework/logger.ts +81 -0
- package/src/index.ts +211 -0
- package/src/lib/action-aggregates.ts +1031 -0
- package/src/lib/action-identity.ts +106 -0
- package/src/lib/actions-store.ts +397 -0
- package/src/lib/ai-pricing.ts +80 -0
- package/src/lib/ai-store/calls.ts +148 -0
- package/src/lib/ai-store/evaluations.test.ts +47 -0
- package/src/lib/ai-store/evaluations.ts +125 -0
- package/src/lib/ai-store/helpers.ts +37 -0
- package/src/lib/ai-store/retention.ts +17 -0
- package/src/lib/ai-store/sessions.ts +277 -0
- package/src/lib/ai-store/spans.ts +109 -0
- package/src/lib/ai-store/types.ts +112 -0
- package/src/lib/ai-store.ts +76 -0
- package/src/lib/alerts-store.ts +458 -0
- package/src/lib/analyses-runner.ts +439 -0
- package/src/lib/analyses-store.test.ts +243 -0
- package/src/lib/analyses-store.ts +454 -0
- package/src/lib/ask.test.ts +302 -0
- package/src/lib/ask.ts +390 -0
- package/src/lib/blob-store-s3.ts +213 -0
- package/src/lib/blob-store.ts +366 -0
- package/src/lib/eval-cases-store.test.ts +171 -0
- package/src/lib/eval-cases-store.ts +1033 -0
- package/src/lib/evidence-references.test.ts +10 -0
- package/src/lib/evidence-references.ts +684 -0
- package/src/lib/hash.ts +26 -0
- package/src/lib/identity-index/action-lookups.ts +805 -0
- package/src/lib/identity-index/constants.ts +1 -0
- package/src/lib/identity-index/key-lookups.ts +199 -0
- package/src/lib/identity-index/mappers.ts +349 -0
- package/src/lib/identity-index/types.ts +196 -0
- package/src/lib/identity-index/user-lookup.ts +137 -0
- package/src/lib/identity-index.test.ts +624 -0
- package/src/lib/identity-index.ts +85 -0
- package/src/lib/json.ts +39 -0
- package/src/lib/llm.ts +263 -0
- package/src/lib/logs-store.ts +188 -0
- package/src/lib/metrics-store.test.ts +189 -0
- package/src/lib/metrics-store.ts +312 -0
- package/src/lib/narrate-gate.test.ts +260 -0
- package/src/lib/narrate-gate.ts +253 -0
- package/src/lib/openai.test.ts +300 -0
- package/src/lib/openai.ts +512 -0
- package/src/lib/otlp.ts +212 -0
- package/src/lib/parse-pprof.test.ts +64 -0
- package/src/lib/parse-pprof.ts +184 -0
- package/src/lib/payload-capture.ts +22 -0
- package/src/lib/projects-store.ts +207 -0
- package/src/lib/propagation-metric.test.ts +196 -0
- package/src/lib/propagation-metric.ts +175 -0
- package/src/lib/sql-db-dialect.test.ts +22 -0
- package/src/lib/sql-db-postgres.ts +229 -0
- package/src/lib/sql-db.ts +166 -0
- package/src/lib/stage6.test.ts +216 -0
- package/src/lib/store/export.ts +65 -0
- package/src/lib/store/helpers.ts +367 -0
- package/src/lib/store/ingest.ts +68 -0
- package/src/lib/store/issues.ts +158 -0
- package/src/lib/store/overview.ts +127 -0
- package/src/lib/store/retention.ts +10 -0
- package/src/lib/store/service-map.ts +384 -0
- package/src/lib/store/trace-detail.bench.ts +76 -0
- package/src/lib/store/trace-detail.test.ts +410 -0
- package/src/lib/store/trace-detail.ts +427 -0
- package/src/lib/store/trace-queries.test.ts +29 -0
- package/src/lib/store/trace-queries.ts +89 -0
- package/src/lib/store-overview.test.ts +77 -0
- package/src/lib/store.ts +96 -0
- package/src/lib/tail-publisher.ts +64 -0
- package/src/lib/test-utils/mem-sql-db.test.ts +93 -0
- package/src/lib/test-utils/mem-sql-db.ts +86 -0
- package/src/lib/ua-parser.ts +61 -0
- package/src/lib/usage-store.ts +567 -0
- package/src/migrations/001_telemetry_spans.sql +28 -0
- package/src/migrations/002_usage_events.sql +24 -0
- package/src/migrations/003_usage_analytics_columns.sql +13 -0
- package/src/migrations/004_logs.sql +19 -0
- package/src/migrations/005_ai_calls.sql +25 -0
- package/src/migrations/006_user_profiles.sql +11 -0
- package/src/migrations/007_session_replay_metadata.sql +10 -0
- package/src/migrations/008_session_replay_storage_bytes.sql +1 -0
- package/src/migrations/009_projects_and_keys.sql +22 -0
- package/src/migrations/010_signal_project_id.sql +15 -0
- package/src/migrations/011_default_project_seed.sql +3 -0
- package/src/migrations/012_alert_rules.sql +17 -0
- package/src/migrations/013_alert_evaluations.sql +13 -0
- package/src/migrations/014_alert_state.sql +9 -0
- package/src/migrations/015_metrics.sql +58 -0
- package/src/migrations/016_metrics_exp_histogram_summary.sql +5 -0
- package/src/migrations/017_span_dropped_counts.sql +11 -0
- package/src/migrations/018_log_flags.sql +8 -0
- package/src/migrations/019_ai_span_payloads.sql +30 -0
- package/src/migrations/020_ai_span_evaluations.sql +33 -0
- package/src/migrations/021_ai_span_session.sql +13 -0
- package/src/migrations/022_span_log_session_id.sql +13 -0
- package/src/migrations/023_service_map_indexes.sql +32 -0
- package/src/migrations/024_analysis_results.sql +49 -0
- package/src/migrations/025_stage6_autopinning_alerts.sql +38 -0
- package/src/migrations/026_obs_dashboard_project_seed.sql +18 -0
- package/src/migrations/027_identity_propagation.sql +50 -0
- package/src/migrations/028_profile_blobs.sql +57 -0
- package/src/migrations/029_telemetry_sdk_name.sql +20 -0
- package/src/migrations/030_analysis_claim.sql +18 -0
- package/src/migrations/031_agent_action_graph.sql +154 -0
- package/src/migrations/032_payload_capture_flags.sql +8 -0
- package/src/migrations/033_eval_cases.sql +52 -0
- package/src/migrations/034_eval_case_results.sql +23 -0
- package/src/migrations/035_metric_exemplars.sql +33 -0
- package/src/migrations/036_trace_instrumentation_gaps.sql +19 -0
- package/src/migrations/037_eval_runs.sql +37 -0
- package/src/migrations/038_tool_call_evidence.sql +11 -0
- package/src/migrations/039_drop_trace_instrumentation_gaps.sql +11 -0
- package/src/migrations/040_evidence_retrieval_refs.sql +50 -0
- package/src/migrations-postgres/001_telemetry_spans.sql +33 -0
- package/src/migrations-postgres/002_usage_events.sql +24 -0
- package/src/migrations-postgres/003_usage_analytics_columns.sql +13 -0
- package/src/migrations-postgres/004_logs.sql +19 -0
- package/src/migrations-postgres/005_ai_calls.sql +25 -0
- package/src/migrations-postgres/006_user_profiles.sql +11 -0
- package/src/migrations-postgres/007_session_replay_metadata.sql +10 -0
- package/src/migrations-postgres/008_session_replay_storage_bytes.sql +2 -0
- package/src/migrations-postgres/009_projects_and_keys.sql +25 -0
- package/src/migrations-postgres/010_signal_project_id.sql +18 -0
- package/src/migrations-postgres/011_default_project_seed.sql +5 -0
- package/src/migrations-postgres/012_alert_rules.sql +17 -0
- package/src/migrations-postgres/013_alert_evaluations.sql +13 -0
- package/src/migrations-postgres/014_alert_state.sql +9 -0
- package/src/migrations-postgres/015_metrics.sql +47 -0
- package/src/migrations-postgres/016_metrics_exp_histogram_summary.sql +1 -0
- package/src/migrations-postgres/017_span_dropped_counts.sql +4 -0
- package/src/migrations-postgres/018_log_flags.sql +2 -0
- package/src/migrations-postgres/019_ai_span_payloads.sql +18 -0
- package/src/migrations-postgres/020_ai_span_evaluations.sql +21 -0
- package/src/migrations-postgres/021_ai_span_session.sql +7 -0
- package/src/migrations-postgres/022_span_log_session_id.sql +7 -0
- package/src/migrations-postgres/023_service_map_indexes.sql +9 -0
- package/src/migrations-postgres/024_analysis_results.sql +41 -0
- package/src/migrations-postgres/025_stage6_autopinning_alerts.sql +17 -0
- package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +14 -0
- package/src/migrations-postgres/027_identity_propagation.sql +27 -0
- package/src/migrations-postgres/028_profile_blobs.sql +48 -0
- package/src/migrations-postgres/029_telemetry_sdk_name.sql +5 -0
- package/src/migrations-postgres/030_analysis_claim.sql +5 -0
- package/src/migrations-postgres/031_agent_action_graph.sql +190 -0
- package/src/migrations-postgres/032_payload_capture_flags.sql +8 -0
- package/src/migrations-postgres/033_eval_cases.sql +55 -0
- package/src/migrations-postgres/034_eval_case_results.sql +26 -0
- package/src/migrations-postgres/035_metric_exemplars.sql +31 -0
- package/src/migrations-postgres/036_trace_instrumentation_gaps.sql +19 -0
- package/src/migrations-postgres/037_eval_runs.sql +40 -0
- package/src/migrations-postgres/038_tool_call_evidence.sql +11 -0
- package/src/migrations-postgres/039_drop_trace_instrumentation_gaps.sql +11 -0
- package/src/migrations-postgres/040_evidence_retrieval_refs.sql +47 -0
- package/src/migrations-postgres/README.md +40 -0
- package/src/otlp/acceptance.test.ts +373 -0
- package/src/otlp/decode/body.ts +127 -0
- package/src/otlp/decode/logs.ts +96 -0
- package/src/otlp/decode/metrics.ts +402 -0
- package/src/otlp/decode/traces.ts +92 -0
- package/src/otlp/decode/values.ts +150 -0
- package/src/otlp/decode.test.ts +800 -0
- package/src/otlp/decode.ts +11 -0
- package/src/otlp/gen/opentelemetry/proto/collector/logs/v1/logs_service_pb.ts +132 -0
- package/src/otlp/gen/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.ts +158 -0
- package/src/otlp/gen/opentelemetry/proto/collector/trace/v1/trace_service_pb.ts +158 -0
- package/src/otlp/gen/opentelemetry/proto/common/v1/common_pb.ts +289 -0
- package/src/otlp/gen/opentelemetry/proto/logs/v1/logs_pb.ts +397 -0
- package/src/otlp/gen/opentelemetry/proto/metrics/v1/metrics_pb.ts +1329 -0
- package/src/otlp/gen/opentelemetry/proto/resource/v1/resource_pb.ts +86 -0
- package/src/otlp/gen/opentelemetry/proto/trace/v1/trace_pb.ts +710 -0
- package/src/otlp/response.test.ts +158 -0
- package/src/otlp/response.ts +129 -0
- package/src/plugins/_context.ts +15 -0
- package/src/plugins/action-graph-processor/enrichers.ts +45 -0
- package/src/plugins/action-graph-processor/redaction.ts +84 -0
- package/src/plugins/action-graph-processor.test.ts +1023 -0
- package/src/plugins/action-graph-processor.ts +1172 -0
- package/src/plugins/action-routes.test.ts +716 -0
- package/src/plugins/action-routes.ts +343 -0
- package/src/plugins/ai-receiver.ts +254 -0
- package/src/plugins/ai-span-payloads-processor.test.ts +90 -0
- package/src/plugins/ai-span-payloads-processor.ts +180 -0
- package/src/plugins/alerts-evaluator.test.ts +91 -0
- package/src/plugins/alerts-evaluator.ts +328 -0
- package/src/plugins/alerts-routes.ts +101 -0
- package/src/plugins/analyses-routes.ts +138 -0
- package/src/plugins/ask-routes.ts +182 -0
- package/src/plugins/bot-filter.ts +19 -0
- package/src/plugins/connected-routes/manifest.ts +531 -0
- package/src/plugins/connected-routes.test.ts +1791 -0
- package/src/plugins/connected-routes.ts +1103 -0
- package/src/plugins/dashboard-routes.ts +41 -0
- package/src/plugins/default-span-enrichment.ts +92 -0
- package/src/plugins/eval-cases-routes.test.ts +391 -0
- package/src/plugins/eval-cases-routes.ts +529 -0
- package/src/plugins/evidence-retrieval-routes.benchmark.test.ts +245 -0
- package/src/plugins/evidence-retrieval-routes.test.ts +932 -0
- package/src/plugins/evidence-retrieval-routes.ts +2179 -0
- package/src/plugins/gen-ai-normalizer.test.ts +680 -0
- package/src/plugins/gen-ai-normalizer.ts +302 -0
- package/src/plugins/identity-receiver.ts +96 -0
- package/src/plugins/issue-enrichment.ts +132 -0
- package/src/plugins/issue-insights.ts +81 -0
- package/src/plugins/logs-receiver.ts +160 -0
- package/src/plugins/metrics-receiver.ts +91 -0
- package/src/plugins/onboarding-routes.ts +54 -0
- package/src/plugins/otlp-receiver.ts +124 -0
- package/src/plugins/platform-routes.ts +165 -0
- package/src/plugins/profile-routes.ts +468 -0
- package/src/plugins/projects-routes.ts +92 -0
- package/src/plugins/query-routes.ts +220 -0
- package/src/plugins/redaction-processor.ts +132 -0
- package/src/plugins/replay-query-routes.test.ts +38 -0
- package/src/plugins/replay-query-routes.ts +199 -0
- package/src/plugins/replay-receiver.ts +81 -0
- package/src/plugins/tail-routes.test.ts +47 -0
- package/src/plugins/tail-routes.ts +42 -0
- package/src/plugins/timeline-routes.ts +244 -0
- package/src/plugins/ua-enrichment.ts +27 -0
- package/src/plugins/usage-normalization.ts +46 -0
- package/src/plugins/usage-privacy.ts +55 -0
- package/src/plugins/usage-query-routes.ts +102 -0
- package/src/plugins/usage-receiver.ts +129 -0
- package/src/plugins/users-query-routes.ts +78 -0
- 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
|
+
};
|