@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,127 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TelemetryOverviewOptions,
|
|
3
|
+
TelemetryOverviewResponse,
|
|
4
|
+
} from "@obsunified/types";
|
|
5
|
+
import type { SqlDb } from "../sql-db";
|
|
6
|
+
import {
|
|
7
|
+
average,
|
|
8
|
+
cutoffIso,
|
|
9
|
+
normalizeService,
|
|
10
|
+
type ParsedSpan,
|
|
11
|
+
percentile,
|
|
12
|
+
} from "./helpers";
|
|
13
|
+
import { fetchSpansForTraceIds, selectTraceCandidates } from "./trace-queries";
|
|
14
|
+
|
|
15
|
+
export async function getTelemetryOverview(
|
|
16
|
+
db: SqlDb,
|
|
17
|
+
options: TelemetryOverviewOptions,
|
|
18
|
+
): Promise<TelemetryOverviewResponse> {
|
|
19
|
+
if (!options.projectId)
|
|
20
|
+
throw new Error("TelemetryStore.getOverview: projectId is required");
|
|
21
|
+
const cutoff = cutoffIso(options.hours);
|
|
22
|
+
const traceLimit = options.limit ?? 30;
|
|
23
|
+
const candidates = await selectTraceCandidates(db, {
|
|
24
|
+
projectId: options.projectId,
|
|
25
|
+
cutoff,
|
|
26
|
+
service: options.service,
|
|
27
|
+
search: options.search,
|
|
28
|
+
status: options.status,
|
|
29
|
+
limit: traceLimit,
|
|
30
|
+
});
|
|
31
|
+
const candidateOrder = new Map(
|
|
32
|
+
candidates.map((candidate, index) => [candidate.trace_id, index]),
|
|
33
|
+
);
|
|
34
|
+
const grouped = new Map<string, ParsedSpan[]>();
|
|
35
|
+
for (const parsed of await fetchSpansForTraceIds(
|
|
36
|
+
db,
|
|
37
|
+
options.projectId,
|
|
38
|
+
candidates.map((candidate) => candidate.trace_id),
|
|
39
|
+
)) {
|
|
40
|
+
const traceSpans = grouped.get(parsed.traceId) ?? [];
|
|
41
|
+
traceSpans.push(parsed);
|
|
42
|
+
grouped.set(parsed.traceId, traceSpans);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const traces = Array.from(grouped.values())
|
|
46
|
+
.map((traceSpans) => {
|
|
47
|
+
const sorted = traceSpans.sort((left, right) =>
|
|
48
|
+
left.startTime.localeCompare(right.startTime),
|
|
49
|
+
);
|
|
50
|
+
const root = sorted.find((span) => !span.parentSpanId) ?? sorted[0];
|
|
51
|
+
return {
|
|
52
|
+
traceId: root.traceId,
|
|
53
|
+
serviceName: normalizeService(root.serviceName),
|
|
54
|
+
spanName: root.spanName,
|
|
55
|
+
statusCode: traceSpans.some((span) => span.statusCode === 2)
|
|
56
|
+
? 2
|
|
57
|
+
: root.statusCode,
|
|
58
|
+
statusMessage:
|
|
59
|
+
traceSpans.find((span) => span.statusCode === 2)?.statusMessage ??
|
|
60
|
+
root.statusMessage,
|
|
61
|
+
startTime: root.startTime,
|
|
62
|
+
endTime: root.endTime,
|
|
63
|
+
durationMs: root.durationMs,
|
|
64
|
+
receivedAt: root.receivedAt,
|
|
65
|
+
spanCount: traceSpans.length,
|
|
66
|
+
errorSpanCount: traceSpans.filter((span) => span.statusCode === 2)
|
|
67
|
+
.length,
|
|
68
|
+
};
|
|
69
|
+
})
|
|
70
|
+
.sort((left, right) => right.receivedAt.localeCompare(left.receivedAt));
|
|
71
|
+
traces.sort(
|
|
72
|
+
(left, right) =>
|
|
73
|
+
(candidateOrder.get(left.traceId) ?? Number.MAX_SAFE_INTEGER) -
|
|
74
|
+
(candidateOrder.get(right.traceId) ?? Number.MAX_SAFE_INTEGER),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const durations = traces.map((trace) => trace.durationMs);
|
|
78
|
+
const errorTraces = traces.filter((trace) => trace.errorSpanCount > 0);
|
|
79
|
+
const serviceMap = new Map<
|
|
80
|
+
string,
|
|
81
|
+
{ traceCount: number; errorTraceCount: number; durations: number[] }
|
|
82
|
+
>();
|
|
83
|
+
for (const trace of traces) {
|
|
84
|
+
const entry = serviceMap.get(trace.serviceName) ?? {
|
|
85
|
+
traceCount: 0,
|
|
86
|
+
errorTraceCount: 0,
|
|
87
|
+
durations: [],
|
|
88
|
+
};
|
|
89
|
+
entry.traceCount += 1;
|
|
90
|
+
entry.errorTraceCount += trace.errorSpanCount > 0 ? 1 : 0;
|
|
91
|
+
entry.durations.push(trace.durationMs);
|
|
92
|
+
serviceMap.set(trace.serviceName, entry);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
summary: {
|
|
97
|
+
totalTraces: traces.length,
|
|
98
|
+
errorTraces: errorTraces.length,
|
|
99
|
+
successTraces: traces.length - errorTraces.length,
|
|
100
|
+
errorRate: traces.length > 0 ? errorTraces.length / traces.length : 0,
|
|
101
|
+
averageDurationMs: Math.round(average(durations)),
|
|
102
|
+
p95DurationMs: Math.round(percentile(durations, 0.95)),
|
|
103
|
+
},
|
|
104
|
+
services: Array.from(serviceMap.entries())
|
|
105
|
+
.map(([serviceName, entry]) => ({
|
|
106
|
+
serviceName,
|
|
107
|
+
traceCount: entry.traceCount,
|
|
108
|
+
errorTraceCount: entry.errorTraceCount,
|
|
109
|
+
errorRate:
|
|
110
|
+
entry.traceCount > 0 ? entry.errorTraceCount / entry.traceCount : 0,
|
|
111
|
+
averageDurationMs: Math.round(average(entry.durations)),
|
|
112
|
+
maxDurationMs: Math.max(...entry.durations, 0),
|
|
113
|
+
}))
|
|
114
|
+
.sort(
|
|
115
|
+
(left, right) =>
|
|
116
|
+
right.traceCount - left.traceCount ||
|
|
117
|
+
left.serviceName.localeCompare(right.serviceName),
|
|
118
|
+
),
|
|
119
|
+
traces: traces.slice(0, options.limit ?? 30),
|
|
120
|
+
windowHours: options.hours,
|
|
121
|
+
filters: {
|
|
122
|
+
service: options.service ?? "all",
|
|
123
|
+
status: options.status ?? "all",
|
|
124
|
+
},
|
|
125
|
+
timestamp: new Date().toISOString(),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SqlDb } from "../sql-db";
|
|
2
|
+
|
|
3
|
+
export async function purgeExpiredTelemetry(db: SqlDb): Promise<number> {
|
|
4
|
+
const now = new Date().toISOString();
|
|
5
|
+
const result = await db
|
|
6
|
+
.prepare("DELETE FROM telemetry_spans WHERE expires_at <= ?")
|
|
7
|
+
.bind(now)
|
|
8
|
+
.run();
|
|
9
|
+
return result.meta?.changes ?? 0;
|
|
10
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { dialectFor, type SqlDb } from "../sql-db";
|
|
2
|
+
import { cutoffIso, percentile } from "./helpers";
|
|
3
|
+
|
|
4
|
+
export interface ServiceMapOptions {
|
|
5
|
+
projectId: string;
|
|
6
|
+
hours: number;
|
|
7
|
+
source?: "all" | "sdk" | "ebpf";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ServiceOperationsOptions {
|
|
11
|
+
projectId: string;
|
|
12
|
+
service: string;
|
|
13
|
+
hours: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function getTelemetryServiceMap(
|
|
17
|
+
db: SqlDb,
|
|
18
|
+
options: ServiceMapOptions,
|
|
19
|
+
) {
|
|
20
|
+
if (!options.projectId)
|
|
21
|
+
throw new Error("TelemetryStore.getServiceMap: projectId is required");
|
|
22
|
+
const cutoff = cutoffIso(options.hours);
|
|
23
|
+
const dialect = dialectFor(db);
|
|
24
|
+
const source = options.source ?? "all";
|
|
25
|
+
|
|
26
|
+
// RFC 0009 — eBPF-derived agents identify themselves via
|
|
27
|
+
// resource_attribute `telemetry.sdk.name`. The set is small and
|
|
28
|
+
// stable enough to inline; expand as new agents land.
|
|
29
|
+
const EBPF_SDK_NAMES = new Set(["beyla", "otel-ebpf-profiler"]);
|
|
30
|
+
const sourceClause =
|
|
31
|
+
source === "ebpf"
|
|
32
|
+
? ` AND telemetry_sdk_name IN (${Array.from(EBPF_SDK_NAMES)
|
|
33
|
+
.map(() => "?")
|
|
34
|
+
.join(",")})`
|
|
35
|
+
: source === "sdk"
|
|
36
|
+
? ` AND (telemetry_sdk_name IS NULL OR telemetry_sdk_name NOT IN (${Array.from(
|
|
37
|
+
EBPF_SDK_NAMES,
|
|
38
|
+
)
|
|
39
|
+
.map(() => "?")
|
|
40
|
+
.join(",")}))`
|
|
41
|
+
: "";
|
|
42
|
+
const sourceBinds: unknown[] =
|
|
43
|
+
source === "all" ? [] : Array.from(EBPF_SDK_NAMES);
|
|
44
|
+
|
|
45
|
+
const nodesResult = await db
|
|
46
|
+
.prepare(
|
|
47
|
+
`SELECT
|
|
48
|
+
service_name,
|
|
49
|
+
COUNT(*) AS span_count,
|
|
50
|
+
SUM(CASE WHEN status_code = 2 THEN 1 ELSE 0 END) AS error_count,
|
|
51
|
+
COUNT(DISTINCT trace_id) AS trace_count
|
|
52
|
+
FROM telemetry_spans
|
|
53
|
+
WHERE project_id = ? AND received_at >= ? AND service_name IS NOT NULL${sourceClause}
|
|
54
|
+
GROUP BY service_name`,
|
|
55
|
+
)
|
|
56
|
+
.bind(options.projectId, cutoff, ...sourceBinds)
|
|
57
|
+
.all<{
|
|
58
|
+
service_name: string;
|
|
59
|
+
span_count: number;
|
|
60
|
+
error_count: number;
|
|
61
|
+
trace_count: number;
|
|
62
|
+
}>();
|
|
63
|
+
|
|
64
|
+
// Edges come from two relationships:
|
|
65
|
+
// 1. Synchronous: child span's parent_span_id points at the parent's
|
|
66
|
+
// span_id within the same trace, and they're in different services.
|
|
67
|
+
// 2. Asynchronous: child span carries a `link` (in links_json) back
|
|
68
|
+
// to a producer span in another service — this is how OpenTelemetry
|
|
69
|
+
// represents Kafka / RabbitMQ / SQS / Pub/Sub dispatch, because
|
|
70
|
+
// consumers can process messages long after producers close their
|
|
71
|
+
// span and a single consume can correspond to many produces.
|
|
72
|
+
// We UNION ALL both — duplicates are accumulated as separate calls,
|
|
73
|
+
// which matches the synchronous semantics (each child = one call).
|
|
74
|
+
// RFC 0009 — applying the source filter to *child* spans for sync
|
|
75
|
+
// edges and to *consumer* spans for async edges. The producer
|
|
76
|
+
// side is left unfiltered (an SDK-instrumented service that
|
|
77
|
+
// publishes to a Kafka topic still belongs in the eBPF view if
|
|
78
|
+
// the consumer is Beyla-instrumented; the edge represents the
|
|
79
|
+
// kernel-observed call, not the producer's instrumentation).
|
|
80
|
+
const childSourceClause =
|
|
81
|
+
source === "ebpf"
|
|
82
|
+
? ` AND c.telemetry_sdk_name IN (${Array.from(EBPF_SDK_NAMES)
|
|
83
|
+
.map(() => "?")
|
|
84
|
+
.join(",")})`
|
|
85
|
+
: source === "sdk"
|
|
86
|
+
? ` AND (c.telemetry_sdk_name IS NULL OR c.telemetry_sdk_name NOT IN (${Array.from(
|
|
87
|
+
EBPF_SDK_NAMES,
|
|
88
|
+
)
|
|
89
|
+
.map(() => "?")
|
|
90
|
+
.join(",")}))`
|
|
91
|
+
: "";
|
|
92
|
+
const consumerSourceClause =
|
|
93
|
+
source === "ebpf"
|
|
94
|
+
? ` AND telemetry_sdk_name IN (${Array.from(EBPF_SDK_NAMES)
|
|
95
|
+
.map(() => "?")
|
|
96
|
+
.join(",")})`
|
|
97
|
+
: source === "sdk"
|
|
98
|
+
? ` AND (telemetry_sdk_name IS NULL OR telemetry_sdk_name NOT IN (${Array.from(
|
|
99
|
+
EBPF_SDK_NAMES,
|
|
100
|
+
)
|
|
101
|
+
.map(() => "?")
|
|
102
|
+
.join(",")}))`
|
|
103
|
+
: "";
|
|
104
|
+
const linkEdgesSql =
|
|
105
|
+
dialect.name === "postgres"
|
|
106
|
+
? `link_edges AS (
|
|
107
|
+
SELECT
|
|
108
|
+
producer.service_name AS source,
|
|
109
|
+
consumer.service_name AS target,
|
|
110
|
+
consumer.status_code AS status_code,
|
|
111
|
+
consumer.duration_ms AS duration_ms,
|
|
112
|
+
consumer.received_at AS received_at,
|
|
113
|
+
CASE WHEN consumer.telemetry_sdk_name IN ('beyla', 'otel-ebpf-profiler') THEN 1 ELSE 0 END AS is_ebpf
|
|
114
|
+
FROM (
|
|
115
|
+
SELECT trace_id, span_id, project_id, service_name,
|
|
116
|
+
status_code, duration_ms, received_at, links_json, telemetry_sdk_name
|
|
117
|
+
FROM telemetry_spans
|
|
118
|
+
WHERE project_id = ?
|
|
119
|
+
AND received_at >= ?
|
|
120
|
+
AND links_json IS NOT NULL
|
|
121
|
+
AND links_json != '[]'${consumerSourceClause}
|
|
122
|
+
) consumer
|
|
123
|
+
CROSS JOIN LATERAL jsonb_array_elements(consumer.links_json::jsonb) link(value)
|
|
124
|
+
JOIN telemetry_spans producer
|
|
125
|
+
ON producer.trace_id = link.value ->> 'traceId'
|
|
126
|
+
AND producer.span_id = link.value ->> 'spanId'
|
|
127
|
+
AND producer.project_id = consumer.project_id
|
|
128
|
+
WHERE producer.received_at >= ?
|
|
129
|
+
AND consumer.service_name IS NOT NULL
|
|
130
|
+
AND producer.service_name IS NOT NULL
|
|
131
|
+
AND producer.service_name != consumer.service_name
|
|
132
|
+
)`
|
|
133
|
+
: `link_edges AS (
|
|
134
|
+
SELECT
|
|
135
|
+
producer.service_name AS source,
|
|
136
|
+
consumer.service_name AS target,
|
|
137
|
+
consumer.status_code AS status_code,
|
|
138
|
+
consumer.duration_ms AS duration_ms,
|
|
139
|
+
consumer.received_at AS received_at,
|
|
140
|
+
CASE WHEN consumer.telemetry_sdk_name IN ('beyla', 'otel-ebpf-profiler') THEN 1 ELSE 0 END AS is_ebpf
|
|
141
|
+
-- Pre-filter consumers in a subquery so json_each is only
|
|
142
|
+
-- invoked on spans that actually carry links. Without this
|
|
143
|
+
-- gate the cross product fans out across every span in the
|
|
144
|
+
-- window and the query times out on tens of thousands of
|
|
145
|
+
-- rows.
|
|
146
|
+
FROM (
|
|
147
|
+
SELECT trace_id, span_id, project_id, service_name,
|
|
148
|
+
status_code, duration_ms, received_at, links_json, telemetry_sdk_name
|
|
149
|
+
FROM telemetry_spans
|
|
150
|
+
WHERE project_id = ?
|
|
151
|
+
AND received_at >= ?
|
|
152
|
+
AND links_json IS NOT NULL
|
|
153
|
+
AND links_json != '[]'${consumerSourceClause}
|
|
154
|
+
) consumer,
|
|
155
|
+
json_each(consumer.links_json) link
|
|
156
|
+
-- Match on (trace_id, span_id) so the lookup hits the
|
|
157
|
+
-- composite PRIMARY KEY index. span_id alone has no index
|
|
158
|
+
-- (only the composite PK), so a span_id-only join would
|
|
159
|
+
-- full-scan per link.
|
|
160
|
+
JOIN telemetry_spans producer
|
|
161
|
+
ON producer.trace_id = json_extract(link.value, '$.traceId')
|
|
162
|
+
AND producer.span_id = json_extract(link.value, '$.spanId')
|
|
163
|
+
AND producer.project_id = consumer.project_id
|
|
164
|
+
WHERE producer.received_at >= ?
|
|
165
|
+
AND consumer.service_name IS NOT NULL
|
|
166
|
+
AND producer.service_name IS NOT NULL
|
|
167
|
+
AND producer.service_name != consumer.service_name
|
|
168
|
+
)`;
|
|
169
|
+
|
|
170
|
+
const edgeRowsResult = await db
|
|
171
|
+
.prepare(
|
|
172
|
+
`WITH parent_child_edges AS (
|
|
173
|
+
SELECT
|
|
174
|
+
p.service_name AS source,
|
|
175
|
+
c.service_name AS target,
|
|
176
|
+
c.status_code AS status_code,
|
|
177
|
+
c.duration_ms AS duration_ms,
|
|
178
|
+
c.received_at AS received_at,
|
|
179
|
+
CASE WHEN c.telemetry_sdk_name IN ('beyla', 'otel-ebpf-profiler') THEN 1 ELSE 0 END AS is_ebpf
|
|
180
|
+
FROM telemetry_spans p
|
|
181
|
+
JOIN telemetry_spans c
|
|
182
|
+
ON c.parent_span_id = p.span_id
|
|
183
|
+
AND c.trace_id = p.trace_id
|
|
184
|
+
AND c.project_id = p.project_id
|
|
185
|
+
WHERE p.project_id = ?
|
|
186
|
+
AND p.received_at >= ?
|
|
187
|
+
AND c.received_at >= ?
|
|
188
|
+
AND p.service_name IS NOT NULL
|
|
189
|
+
AND c.service_name IS NOT NULL
|
|
190
|
+
AND p.service_name != c.service_name${childSourceClause}
|
|
191
|
+
),
|
|
192
|
+
${linkEdgesSql}
|
|
193
|
+
SELECT * FROM parent_child_edges
|
|
194
|
+
UNION ALL
|
|
195
|
+
SELECT * FROM link_edges
|
|
196
|
+
ORDER BY received_at DESC
|
|
197
|
+
LIMIT 50000`,
|
|
198
|
+
)
|
|
199
|
+
.bind(
|
|
200
|
+
options.projectId,
|
|
201
|
+
cutoff,
|
|
202
|
+
cutoff,
|
|
203
|
+
...sourceBinds,
|
|
204
|
+
options.projectId,
|
|
205
|
+
cutoff,
|
|
206
|
+
...sourceBinds,
|
|
207
|
+
cutoff,
|
|
208
|
+
)
|
|
209
|
+
.all<{
|
|
210
|
+
source: string;
|
|
211
|
+
target: string;
|
|
212
|
+
status_code: number;
|
|
213
|
+
duration_ms: number;
|
|
214
|
+
is_ebpf: number;
|
|
215
|
+
}>();
|
|
216
|
+
|
|
217
|
+
interface EdgeAcc {
|
|
218
|
+
source: string;
|
|
219
|
+
target: string;
|
|
220
|
+
calls: number;
|
|
221
|
+
errors: number;
|
|
222
|
+
durations: number[];
|
|
223
|
+
isEbpf: boolean;
|
|
224
|
+
}
|
|
225
|
+
const edgeMap = new Map<string, EdgeAcc>();
|
|
226
|
+
for (const row of edgeRowsResult.results ?? []) {
|
|
227
|
+
const key = `${row.source}|${row.target}`;
|
|
228
|
+
let acc = edgeMap.get(key);
|
|
229
|
+
if (!acc) {
|
|
230
|
+
acc = {
|
|
231
|
+
source: row.source,
|
|
232
|
+
target: row.target,
|
|
233
|
+
calls: 0,
|
|
234
|
+
errors: 0,
|
|
235
|
+
durations: [],
|
|
236
|
+
isEbpf: false,
|
|
237
|
+
};
|
|
238
|
+
edgeMap.set(key, acc);
|
|
239
|
+
}
|
|
240
|
+
acc.calls += 1;
|
|
241
|
+
if (row.status_code === 2) acc.errors += 1;
|
|
242
|
+
acc.durations.push(row.duration_ms ?? 0);
|
|
243
|
+
if (row.is_ebpf === 1) acc.isEbpf = true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const windowSeconds = Math.max(1, options.hours * 3600);
|
|
247
|
+
const edges = Array.from(edgeMap.values()).map((e) => ({
|
|
248
|
+
source: e.source,
|
|
249
|
+
target: e.target,
|
|
250
|
+
calls: e.calls,
|
|
251
|
+
errors: e.errors,
|
|
252
|
+
errorRate: e.calls > 0 ? e.errors / e.calls : 0,
|
|
253
|
+
p50DurationMs: percentile(e.durations, 0.5),
|
|
254
|
+
p95DurationMs: percentile(e.durations, 0.95),
|
|
255
|
+
rps: e.calls / windowSeconds,
|
|
256
|
+
isEbpf: e.isEbpf,
|
|
257
|
+
}));
|
|
258
|
+
|
|
259
|
+
const nodes = (nodesResult.results ?? []).map((r) => ({
|
|
260
|
+
service: r.service_name,
|
|
261
|
+
spanCount: r.span_count,
|
|
262
|
+
errorCount: r.error_count,
|
|
263
|
+
traceCount: r.trace_count,
|
|
264
|
+
errorRate: r.span_count > 0 ? r.error_count / r.span_count : 0,
|
|
265
|
+
}));
|
|
266
|
+
|
|
267
|
+
return { nodes, edges };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function getTelemetryServiceOperations(
|
|
271
|
+
db: SqlDb,
|
|
272
|
+
options: ServiceOperationsOptions,
|
|
273
|
+
) {
|
|
274
|
+
if (!options.projectId)
|
|
275
|
+
throw new Error(
|
|
276
|
+
"TelemetryStore.getServiceOperations: projectId is required",
|
|
277
|
+
);
|
|
278
|
+
const cutoff = cutoffIso(options.hours);
|
|
279
|
+
|
|
280
|
+
// Top operations — group by span_name, accumulate durations in JS.
|
|
281
|
+
const opsResult = await db
|
|
282
|
+
.prepare(
|
|
283
|
+
// One row per span; calls/errors/percentiles are aggregated per
|
|
284
|
+
// span_name in JS below. Do NOT reintroduce COUNT()/SUM() without
|
|
285
|
+
// a GROUP BY — that collapses the result to a single row in SQLite.
|
|
286
|
+
`SELECT
|
|
287
|
+
span_name,
|
|
288
|
+
status_code,
|
|
289
|
+
duration_ms
|
|
290
|
+
FROM telemetry_spans
|
|
291
|
+
WHERE project_id = ?
|
|
292
|
+
AND service_name = ?
|
|
293
|
+
AND received_at >= ?
|
|
294
|
+
ORDER BY received_at DESC
|
|
295
|
+
LIMIT 20000`,
|
|
296
|
+
)
|
|
297
|
+
.bind(options.projectId, options.service, cutoff)
|
|
298
|
+
.all<{
|
|
299
|
+
span_name: string;
|
|
300
|
+
status_code: number | null;
|
|
301
|
+
duration_ms: number;
|
|
302
|
+
}>();
|
|
303
|
+
|
|
304
|
+
interface OpAcc {
|
|
305
|
+
calls: number;
|
|
306
|
+
errors: number;
|
|
307
|
+
durations: number[];
|
|
308
|
+
}
|
|
309
|
+
const opMap = new Map<string, OpAcc>();
|
|
310
|
+
let totalSpans = 0;
|
|
311
|
+
let totalErrors = 0;
|
|
312
|
+
for (const row of opsResult.results ?? []) {
|
|
313
|
+
const isError = row.status_code === 2;
|
|
314
|
+
totalSpans += 1;
|
|
315
|
+
if (isError) totalErrors += 1;
|
|
316
|
+
let acc = opMap.get(row.span_name);
|
|
317
|
+
if (!acc) {
|
|
318
|
+
acc = { calls: 0, errors: 0, durations: [] };
|
|
319
|
+
opMap.set(row.span_name, acc);
|
|
320
|
+
}
|
|
321
|
+
acc.calls += 1;
|
|
322
|
+
if (isError) acc.errors += 1;
|
|
323
|
+
acc.durations.push(row.duration_ms ?? 0);
|
|
324
|
+
}
|
|
325
|
+
const operations = Array.from(opMap.entries())
|
|
326
|
+
.map(([spanName, acc]) => ({
|
|
327
|
+
spanName,
|
|
328
|
+
calls: acc.calls,
|
|
329
|
+
errors: acc.errors,
|
|
330
|
+
errorRate: acc.calls > 0 ? acc.errors / acc.calls : 0,
|
|
331
|
+
p50DurationMs: percentile(acc.durations, 0.5),
|
|
332
|
+
p95DurationMs: percentile(acc.durations, 0.95),
|
|
333
|
+
}))
|
|
334
|
+
.sort((l, r) => r.calls - l.calls)
|
|
335
|
+
.slice(0, 12);
|
|
336
|
+
|
|
337
|
+
// Recent error spans for triage.
|
|
338
|
+
const errorRows = await db
|
|
339
|
+
.prepare(
|
|
340
|
+
`SELECT trace_id, span_id, span_name, status_message, duration_ms, start_time
|
|
341
|
+
FROM telemetry_spans
|
|
342
|
+
WHERE project_id = ?
|
|
343
|
+
AND service_name = ?
|
|
344
|
+
AND received_at >= ?
|
|
345
|
+
AND status_code = 2
|
|
346
|
+
ORDER BY received_at DESC
|
|
347
|
+
LIMIT 10`,
|
|
348
|
+
)
|
|
349
|
+
.bind(options.projectId, options.service, cutoff)
|
|
350
|
+
.all<{
|
|
351
|
+
trace_id: string;
|
|
352
|
+
span_id: string;
|
|
353
|
+
span_name: string;
|
|
354
|
+
status_message: string | null;
|
|
355
|
+
duration_ms: number;
|
|
356
|
+
start_time: string;
|
|
357
|
+
}>();
|
|
358
|
+
|
|
359
|
+
// Distinct trace count for this service.
|
|
360
|
+
const traceCountRow = await db
|
|
361
|
+
.prepare(
|
|
362
|
+
`SELECT COUNT(DISTINCT trace_id) AS trace_count
|
|
363
|
+
FROM telemetry_spans
|
|
364
|
+
WHERE project_id = ? AND service_name = ? AND received_at >= ?`,
|
|
365
|
+
)
|
|
366
|
+
.bind(options.projectId, options.service, cutoff)
|
|
367
|
+
.first<{ trace_count: number }>();
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
service: options.service,
|
|
371
|
+
spanCount: totalSpans,
|
|
372
|
+
traceCount: traceCountRow?.trace_count ?? 0,
|
|
373
|
+
errorCount: totalErrors,
|
|
374
|
+
operations,
|
|
375
|
+
recentErrors: (errorRows.results ?? []).map((r) => ({
|
|
376
|
+
traceId: r.trace_id,
|
|
377
|
+
spanId: r.span_id,
|
|
378
|
+
spanName: r.span_name,
|
|
379
|
+
statusMessage: r.status_message,
|
|
380
|
+
durationMs: r.duration_ms,
|
|
381
|
+
startTime: r.start_time,
|
|
382
|
+
})),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { TelemetrySpanDetail } from "@obsunified/types";
|
|
2
|
+
import { bench, describe } from "vitest";
|
|
3
|
+
import { buildTraceInstrumentationGaps } from "./trace-detail";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Q1 benchmark — read-time gap computation cost vs trace size.
|
|
7
|
+
*
|
|
8
|
+
* Since PR #21, trace instrumentation gaps are computed lazily at read time
|
|
9
|
+
* from a trace's spans (`buildTraceInstrumentationGaps`) instead of being
|
|
10
|
+
* materialized on ingest. This benchmark measures that pure-compute cost as a
|
|
11
|
+
* function of spans-per-trace, to answer: is read-time computation fast enough
|
|
12
|
+
* to serve on a trace-detail view without a cache, even for very large traces?
|
|
13
|
+
*
|
|
14
|
+
* This measures the trace-detail-page hot path, where spans are already loaded
|
|
15
|
+
* in memory and passed straight into the gap computation. The standalone
|
|
16
|
+
* `/internal/.../gaps` endpoint additionally pays a project-scoped span SELECT
|
|
17
|
+
* + JSON parse; that DB-inclusive end-to-end cost is captured separately by the
|
|
18
|
+
* read-path child-span instrumentation (the other half of "Q1 = Both"), not
|
|
19
|
+
* here.
|
|
20
|
+
*
|
|
21
|
+
* Run: `pnpm --filter @obsunified/collector bench` (excluded from `pnpm test`).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const SIZES = [10, 100, 500, 1_000, 5_000, 10_000];
|
|
25
|
+
|
|
26
|
+
// Deterministic synthetic trace generator (no Math.random — reproducible runs).
|
|
27
|
+
// Builds a tree with branching factor 2 and durations that taper with depth so
|
|
28
|
+
// the gap heuristic does real work (self-time/ratio math + a child reduce for
|
|
29
|
+
// every span). Returns fully-formed TelemetrySpanDetail objects so we exercise
|
|
30
|
+
// the exact code path the read handler uses.
|
|
31
|
+
function makeTrace(spanCount: number): {
|
|
32
|
+
spans: TelemetrySpanDetail[];
|
|
33
|
+
rootDurationMs: number;
|
|
34
|
+
} {
|
|
35
|
+
const BRANCH = 2;
|
|
36
|
+
const baseStart = Date.parse("2026-06-01T00:00:00.000Z");
|
|
37
|
+
const spans: TelemetrySpanDetail[] = [];
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < spanCount; i++) {
|
|
40
|
+
const parentIndex = i === 0 ? null : Math.floor((i - 1) / BRANCH);
|
|
41
|
+
const depth = i === 0 ? 0 : Math.floor(Math.log2(i + 1));
|
|
42
|
+
// Duration tapers with depth; kept > 0 so ratio math runs.
|
|
43
|
+
const durationMs = Math.max(1, 2_000 - depth * 150);
|
|
44
|
+
const startMs = baseStart + i; // monotonic, distinct
|
|
45
|
+
spans.push({
|
|
46
|
+
traceId: "bench-trace",
|
|
47
|
+
spanId: `s${i}`,
|
|
48
|
+
parentSpanId: parentIndex === null ? null : `s${parentIndex}`,
|
|
49
|
+
serviceName: `svc-${i % 8}`,
|
|
50
|
+
scopeName: null,
|
|
51
|
+
scopeVersion: null,
|
|
52
|
+
spanName: `op.${i % 32}`,
|
|
53
|
+
spanKind: 2,
|
|
54
|
+
statusCode: 1,
|
|
55
|
+
statusMessage: null,
|
|
56
|
+
startTime: new Date(startMs).toISOString(),
|
|
57
|
+
endTime: new Date(startMs + durationMs).toISOString(),
|
|
58
|
+
durationMs,
|
|
59
|
+
attributes: { "http.method": "GET", "http.route": `/r/${i % 16}` },
|
|
60
|
+
resourceAttributes: { "service.name": `svc-${i % 8}` },
|
|
61
|
+
events: [],
|
|
62
|
+
links: [],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { spans, rootDurationMs: spans[0]?.durationMs ?? 0 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("buildTraceInstrumentationGaps (read-time gap compute)", () => {
|
|
70
|
+
for (const size of SIZES) {
|
|
71
|
+
const { spans, rootDurationMs } = makeTrace(size);
|
|
72
|
+
bench(`${size} spans`, () => {
|
|
73
|
+
buildTraceInstrumentationGaps("bench-trace", spans, rootDurationMs);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|