@tungthedev/streams-server 0.2.0
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/CODE_OF_CONDUCT.md +45 -0
- package/CONTRIBUTING.md +76 -0
- package/LICENSE +201 -0
- package/README.md +58 -0
- package/SECURITY.md +42 -0
- package/bin/prisma-streams-server +2 -0
- package/package.json +46 -0
- package/src/app.ts +583 -0
- package/src/app_core.ts +3144 -0
- package/src/app_local.ts +206 -0
- package/src/auth.ts +124 -0
- package/src/auto_tune.ts +69 -0
- package/src/backpressure.ts +66 -0
- package/src/bootstrap.ts +613 -0
- package/src/compute/demo_entry.ts +415 -0
- package/src/compute/demo_site.ts +1242 -0
- package/src/compute/entry.ts +19 -0
- package/src/compute/package_entry.ts +4 -0
- package/src/compute/virtual-modules.d.ts +15 -0
- package/src/compute/worker_module_url.ts +9 -0
- package/src/concurrency_gate.ts +108 -0
- package/src/config.ts +402 -0
- package/src/db/bootstrap_store.ts +9 -0
- package/src/db/db.ts +2424 -0
- package/src/db/schema.ts +925 -0
- package/src/db/sqlite_manifest_snapshot.ts +81 -0
- package/src/db/sqlite_touch_store.ts +491 -0
- package/src/db/sqlite_wal_store.ts +472 -0
- package/src/details/full_mode_details.ts +568 -0
- package/src/expiry_sweeper.ts +47 -0
- package/src/foreground_activity.ts +55 -0
- package/src/hist.ts +169 -0
- package/src/index/binary_fuse.ts +379 -0
- package/src/index/indexer.ts +947 -0
- package/src/index/lexicon_file_cache.ts +261 -0
- package/src/index/lexicon_format.ts +93 -0
- package/src/index/lexicon_indexer.ts +863 -0
- package/src/index/run_cache.ts +84 -0
- package/src/index/run_format.ts +213 -0
- package/src/index/schedule.ts +28 -0
- package/src/index/secondary_indexer.ts +901 -0
- package/src/index/secondary_schema.ts +105 -0
- package/src/ingest.ts +309 -0
- package/src/lens/lens.ts +501 -0
- package/src/manifest.ts +249 -0
- package/src/memory.ts +334 -0
- package/src/metrics.ts +147 -0
- package/src/metrics_emitter.ts +83 -0
- package/src/notifier.ts +180 -0
- package/src/objectstore/accounting.ts +151 -0
- package/src/objectstore/interface.ts +13 -0
- package/src/objectstore/mock_r2.ts +269 -0
- package/src/objectstore/null.ts +32 -0
- package/src/objectstore/r2.ts +318 -0
- package/src/observe/pairing.ts +61 -0
- package/src/observe/request.ts +772 -0
- package/src/offset.ts +70 -0
- package/src/postgres/bootstrap.ts +269 -0
- package/src/postgres/companions.ts +197 -0
- package/src/postgres/control_restore.ts +109 -0
- package/src/postgres/details.ts +189 -0
- package/src/postgres/lexicon_index.ts +260 -0
- package/src/postgres/routing_index.ts +189 -0
- package/src/postgres/rows.ts +132 -0
- package/src/postgres/schema.ts +355 -0
- package/src/postgres/secondary_index.ts +238 -0
- package/src/postgres/segments.ts +900 -0
- package/src/postgres/stats.ts +103 -0
- package/src/postgres/store.ts +947 -0
- package/src/postgres/touch.ts +591 -0
- package/src/postgres/types.ts +32 -0
- package/src/profiles/evlog/schema.ts +234 -0
- package/src/profiles/evlog.ts +473 -0
- package/src/profiles/generic.ts +51 -0
- package/src/profiles/index.ts +237 -0
- package/src/profiles/metrics/block_format.ts +109 -0
- package/src/profiles/metrics/normalize.ts +366 -0
- package/src/profiles/metrics/schema.ts +319 -0
- package/src/profiles/metrics.ts +83 -0
- package/src/profiles/otelTraces/normalize.ts +955 -0
- package/src/profiles/otelTraces/otlp.ts +1002 -0
- package/src/profiles/otelTraces/schema.ts +408 -0
- package/src/profiles/otelTraces.ts +390 -0
- package/src/profiles/profile.ts +284 -0
- package/src/profiles/stateProtocol/change_event_conformance.typecheck.ts +35 -0
- package/src/profiles/stateProtocol/changes.ts +24 -0
- package/src/profiles/stateProtocol/ingest.ts +115 -0
- package/src/profiles/stateProtocol/routes.ts +511 -0
- package/src/profiles/stateProtocol/types.ts +6 -0
- package/src/profiles/stateProtocol/validation.ts +51 -0
- package/src/profiles/stateProtocol.ts +107 -0
- package/src/read_filter.ts +468 -0
- package/src/reader.ts +2986 -0
- package/src/runtime/hash.ts +156 -0
- package/src/runtime/hash_vendor/LICENSE.hash-wasm +38 -0
- package/src/runtime/hash_vendor/NOTICE.md +8 -0
- package/src/runtime/hash_vendor/xxhash3.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash32.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash64.umd.min.cjs +7 -0
- package/src/runtime/host_runtime.ts +5 -0
- package/src/runtime_memory.ts +200 -0
- package/src/runtime_memory_sampler.ts +237 -0
- package/src/schema/lens_schema.ts +290 -0
- package/src/schema/proof.ts +547 -0
- package/src/schema/read_json.ts +51 -0
- package/src/schema/registry.ts +966 -0
- package/src/search/agg_format.ts +638 -0
- package/src/search/aggregate.ts +409 -0
- package/src/search/binary/codec.ts +162 -0
- package/src/search/binary/docset.ts +67 -0
- package/src/search/binary/restart_strings.ts +181 -0
- package/src/search/binary/varint.ts +34 -0
- package/src/search/bitset.ts +19 -0
- package/src/search/col_format.ts +382 -0
- package/src/search/col_runtime.ts +59 -0
- package/src/search/column_encoding.ts +43 -0
- package/src/search/companion_file_cache.ts +319 -0
- package/src/search/companion_format.ts +327 -0
- package/src/search/companion_manager.ts +1305 -0
- package/src/search/companion_plan.ts +229 -0
- package/src/search/exact_format.ts +281 -0
- package/src/search/exact_runtime.ts +55 -0
- package/src/search/fts_format.ts +423 -0
- package/src/search/fts_runtime.ts +333 -0
- package/src/search/query.ts +875 -0
- package/src/search/schema.ts +245 -0
- package/src/segment/cache.ts +270 -0
- package/src/segment/cached_segment.ts +89 -0
- package/src/segment/format.ts +403 -0
- package/src/segment/segmenter.ts +412 -0
- package/src/segment/segmenter_worker.ts +72 -0
- package/src/segment/segmenter_workers.ts +130 -0
- package/src/server.ts +264 -0
- package/src/server_auto_tune.ts +158 -0
- package/src/sqlite/adapter.ts +335 -0
- package/src/sqlite/runtime_stats.ts +163 -0
- package/src/stats.ts +205 -0
- package/src/store/append.ts +50 -0
- package/src/store/bootstrap_restore_store.ts +71 -0
- package/src/store/capabilities.ts +86 -0
- package/src/store/full_mode_details_store.ts +71 -0
- package/src/store/index_store.ts +104 -0
- package/src/store/profile_touch_store.ts +1 -0
- package/src/store/rows.ts +144 -0
- package/src/store/schema_profile_store.ts +73 -0
- package/src/store/schema_publication.ts +6 -0
- package/src/store/segment_manifest_store.ts +129 -0
- package/src/store/segment_read_store.ts +22 -0
- package/src/store/stats_accounting_store.ts +83 -0
- package/src/store/touch_store.ts +98 -0
- package/src/store/wal_store.ts +21 -0
- package/src/stream_size_reconciler.ts +100 -0
- package/src/touch/canonical_change.ts +7 -0
- package/src/touch/live_keys.ts +158 -0
- package/src/touch/live_metrics.ts +841 -0
- package/src/touch/live_templates.ts +449 -0
- package/src/touch/manager.ts +1292 -0
- package/src/touch/process_batch.ts +576 -0
- package/src/touch/processor_worker.ts +85 -0
- package/src/touch/spec.ts +459 -0
- package/src/touch/touch_journal.ts +771 -0
- package/src/touch/touch_key_id.ts +20 -0
- package/src/touch/worker_pool.ts +191 -0
- package/src/touch/worker_protocol.ts +57 -0
- package/src/types/proper-lockfile.d.ts +1 -0
- package/src/uploader.ts +358 -0
- package/src/util/base32_crockford.ts +81 -0
- package/src/util/bloom256.ts +67 -0
- package/src/util/byte_lru.ts +73 -0
- package/src/util/cleanup.ts +22 -0
- package/src/util/crc32c.ts +29 -0
- package/src/util/ds_error.ts +15 -0
- package/src/util/duration.ts +17 -0
- package/src/util/endian.ts +53 -0
- package/src/util/json_pointer.ts +148 -0
- package/src/util/log.ts +25 -0
- package/src/util/lru.ts +53 -0
- package/src/util/retry.ts +35 -0
- package/src/util/siphash.ts +71 -0
- package/src/util/stream_paths.ts +50 -0
- package/src/util/time.ts +14 -0
- package/src/util/yield.ts +3 -0
- package/src/util/zstd.ts +24 -0
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
import type { SearchHit, SearchResultBatch } from "../reader";
|
|
3
|
+
import type { UnifiedTimelineItem } from "../profiles";
|
|
4
|
+
|
|
5
|
+
export type ObserveRequestInput = {
|
|
6
|
+
streams: {
|
|
7
|
+
events?: string;
|
|
8
|
+
traces?: string;
|
|
9
|
+
};
|
|
10
|
+
lookup: {
|
|
11
|
+
requestId: string | null;
|
|
12
|
+
traceId: string | null;
|
|
13
|
+
spanId: string | null;
|
|
14
|
+
};
|
|
15
|
+
time: {
|
|
16
|
+
from: string | null;
|
|
17
|
+
to: string | null;
|
|
18
|
+
paddingMs: number;
|
|
19
|
+
};
|
|
20
|
+
include: {
|
|
21
|
+
events: boolean;
|
|
22
|
+
trace: boolean;
|
|
23
|
+
timeline: boolean;
|
|
24
|
+
raw: boolean;
|
|
25
|
+
};
|
|
26
|
+
limits: {
|
|
27
|
+
events: number;
|
|
28
|
+
spans: number;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ObserveSearchCoverage = {
|
|
33
|
+
searched: boolean;
|
|
34
|
+
complete: boolean;
|
|
35
|
+
timed_out: boolean;
|
|
36
|
+
limit_reached: boolean;
|
|
37
|
+
hits: number;
|
|
38
|
+
unique_hits: number;
|
|
39
|
+
query_count: number;
|
|
40
|
+
batch_count: number;
|
|
41
|
+
total: { value: number; relation: "eq" | "gte" };
|
|
42
|
+
index_families_used: string[];
|
|
43
|
+
scanned_tail_docs: number;
|
|
44
|
+
scanned_segments: number;
|
|
45
|
+
possible_missing_events_upper_bound: number;
|
|
46
|
+
queries: ObserveSearchQueryCoverage[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type ObserveSearchQueryCoverage = {
|
|
50
|
+
q: string;
|
|
51
|
+
hits: number;
|
|
52
|
+
total: { value: number; relation: "eq" | "gte" };
|
|
53
|
+
pages: number;
|
|
54
|
+
complete: boolean;
|
|
55
|
+
timed_out: boolean;
|
|
56
|
+
limit_reached: boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type TraceTreeNode = {
|
|
60
|
+
spanId: string;
|
|
61
|
+
parentSpanId: string | null;
|
|
62
|
+
children: TraceTreeNode[];
|
|
63
|
+
depth: number;
|
|
64
|
+
service: string | null;
|
|
65
|
+
name: string;
|
|
66
|
+
kind: string;
|
|
67
|
+
startTime: string;
|
|
68
|
+
endTime: string | null;
|
|
69
|
+
duration: number | null;
|
|
70
|
+
statusCode: "unset" | "ok" | "error";
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type ServiceEdge = {
|
|
74
|
+
from: string;
|
|
75
|
+
to: string;
|
|
76
|
+
count: number;
|
|
77
|
+
errorCount: number;
|
|
78
|
+
latency: {
|
|
79
|
+
count: number;
|
|
80
|
+
sum: number;
|
|
81
|
+
min: number | null;
|
|
82
|
+
max: number | null;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type TraceError = {
|
|
87
|
+
spanId: string;
|
|
88
|
+
service: string | null;
|
|
89
|
+
name: string;
|
|
90
|
+
time: string | null;
|
|
91
|
+
type: string | null;
|
|
92
|
+
message: string | null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type TraceDetails = {
|
|
96
|
+
traceId: string | null;
|
|
97
|
+
rootSpanId: string | null;
|
|
98
|
+
spans: Record<string, unknown>[];
|
|
99
|
+
tree: TraceTreeNode[];
|
|
100
|
+
serviceMap: ServiceEdge[];
|
|
101
|
+
criticalPath: string[];
|
|
102
|
+
errors: TraceError[];
|
|
103
|
+
partial: boolean;
|
|
104
|
+
missingParents: string[];
|
|
105
|
+
duplicateSpans: number;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
109
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function stringField(record: Record<string, unknown>, field: string): string | null {
|
|
113
|
+
const value = record[field];
|
|
114
|
+
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function numberField(record: Record<string, unknown>, field: string): number | null {
|
|
118
|
+
const value = record[field];
|
|
119
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function nestedObject(record: Record<string, unknown>, field: string): Record<string, unknown> {
|
|
123
|
+
const value = record[field];
|
|
124
|
+
return isPlainObject(value) ? value : {};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function pickFields(record: Record<string, unknown>, fields: readonly string[]): Record<string, unknown> {
|
|
128
|
+
const out: Record<string, unknown> = {};
|
|
129
|
+
for (const field of fields) {
|
|
130
|
+
if (Object.prototype.hasOwnProperty.call(record, field)) out[field] = structuredClone(record[field]);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function nonEmptyRecord(record: Record<string, unknown>): Record<string, unknown> | null {
|
|
136
|
+
return Object.keys(record).length === 0 ? null : record;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function compactNested(record: Record<string, unknown>, field: string, fields: readonly string[]): Record<string, unknown> | null {
|
|
140
|
+
const nested = nestedObject(record, field);
|
|
141
|
+
return nonEmptyRecord(pickFields(nested, fields));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseOptionalString(raw: unknown, path: string): Result<string | null, { message: string }> {
|
|
145
|
+
if (raw === undefined || raw === null) return Result.ok(null);
|
|
146
|
+
if (typeof raw !== "string") return Result.err({ message: `${path} must be a string` });
|
|
147
|
+
const trimmed = raw.trim();
|
|
148
|
+
return Result.ok(trimmed === "" ? null : trimmed);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseBoolean(raw: unknown, fallback: boolean, path: string): Result<boolean, { message: string }> {
|
|
152
|
+
if (raw === undefined) return Result.ok(fallback);
|
|
153
|
+
if (typeof raw !== "boolean") return Result.err({ message: `${path} must be boolean` });
|
|
154
|
+
return Result.ok(raw);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseLimit(raw: unknown, fallback: number, max: number, path: string): Result<number, { message: string }> {
|
|
158
|
+
if (raw === undefined) return Result.ok(fallback);
|
|
159
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || !Number.isInteger(raw) || raw <= 0 || raw > max) {
|
|
160
|
+
return Result.err({ message: `${path} must be an integer between 1 and ${max}` });
|
|
161
|
+
}
|
|
162
|
+
return Result.ok(raw);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseTime(raw: unknown): Result<ObserveRequestInput["time"], { message: string }> {
|
|
166
|
+
if (raw === undefined) return Result.ok({ from: null, to: null, paddingMs: 0 });
|
|
167
|
+
if (!isPlainObject(raw)) return Result.err({ message: "time must be an object" });
|
|
168
|
+
const fromRes = parseOptionalString(raw.from, "time.from");
|
|
169
|
+
if (Result.isError(fromRes)) return fromRes;
|
|
170
|
+
const toRes = parseOptionalString(raw.to, "time.to");
|
|
171
|
+
if (Result.isError(toRes)) return toRes;
|
|
172
|
+
for (const [path, value] of [
|
|
173
|
+
["time.from", fromRes.value],
|
|
174
|
+
["time.to", toRes.value],
|
|
175
|
+
] as const) {
|
|
176
|
+
if (value != null && Number.isNaN(Date.parse(value))) return Result.err({ message: `${path} must be an ISO timestamp` });
|
|
177
|
+
}
|
|
178
|
+
const paddingRaw = raw.paddingMs ?? raw.padding_ms;
|
|
179
|
+
if (paddingRaw === undefined) return Result.ok({ from: fromRes.value, to: toRes.value, paddingMs: 0 });
|
|
180
|
+
if (typeof paddingRaw !== "number" || !Number.isFinite(paddingRaw) || paddingRaw < 0 || paddingRaw > 86_400_000) {
|
|
181
|
+
return Result.err({ message: "time.paddingMs must be a non-negative number no greater than 86400000" });
|
|
182
|
+
}
|
|
183
|
+
return Result.ok({ from: fromRes.value, to: toRes.value, paddingMs: Math.trunc(paddingRaw) });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function parseObserveRequestResult(raw: unknown): Result<ObserveRequestInput, { message: string }> {
|
|
187
|
+
if (!isPlainObject(raw)) return Result.err({ message: "observe request must be an object" });
|
|
188
|
+
const streamsRaw = raw.streams;
|
|
189
|
+
if (!isPlainObject(streamsRaw)) return Result.err({ message: "streams must be an object" });
|
|
190
|
+
const eventsStreamRes = parseOptionalString(streamsRaw.events, "streams.events");
|
|
191
|
+
if (Result.isError(eventsStreamRes)) return eventsStreamRes;
|
|
192
|
+
const tracesStreamRes = parseOptionalString(streamsRaw.traces, "streams.traces");
|
|
193
|
+
if (Result.isError(tracesStreamRes)) return tracesStreamRes;
|
|
194
|
+
|
|
195
|
+
const lookupRaw = raw.lookup;
|
|
196
|
+
if (!isPlainObject(lookupRaw)) return Result.err({ message: "lookup must be an object" });
|
|
197
|
+
const requestIdRes = parseOptionalString(lookupRaw.requestId, "lookup.requestId");
|
|
198
|
+
if (Result.isError(requestIdRes)) return requestIdRes;
|
|
199
|
+
const traceIdRes = parseOptionalString(lookupRaw.traceId, "lookup.traceId");
|
|
200
|
+
if (Result.isError(traceIdRes)) return traceIdRes;
|
|
201
|
+
const spanIdRes = parseOptionalString(lookupRaw.spanId, "lookup.spanId");
|
|
202
|
+
if (Result.isError(spanIdRes)) return spanIdRes;
|
|
203
|
+
const lookupCount = [requestIdRes.value, traceIdRes.value, spanIdRes.value].filter((value) => value != null).length;
|
|
204
|
+
if (lookupCount !== 1) return Result.err({ message: "lookup must include exactly one of requestId, traceId, or spanId" });
|
|
205
|
+
|
|
206
|
+
const includeRaw = isPlainObject(raw.include) ? raw.include : {};
|
|
207
|
+
const includeEventsRes = parseBoolean(includeRaw.events, true, "include.events");
|
|
208
|
+
if (Result.isError(includeEventsRes)) return includeEventsRes;
|
|
209
|
+
const includeTraceRes = parseBoolean(includeRaw.trace, true, "include.trace");
|
|
210
|
+
if (Result.isError(includeTraceRes)) return includeTraceRes;
|
|
211
|
+
const includeTimelineRes = parseBoolean(includeRaw.timeline, true, "include.timeline");
|
|
212
|
+
if (Result.isError(includeTimelineRes)) return includeTimelineRes;
|
|
213
|
+
const includeRawRes = parseBoolean(includeRaw.raw, false, "include.raw");
|
|
214
|
+
if (Result.isError(includeRawRes)) return includeRawRes;
|
|
215
|
+
if (includeEventsRes.value && !eventsStreamRes.value) return Result.err({ message: "streams.events is required when include.events is true" });
|
|
216
|
+
if (includeTraceRes.value && !tracesStreamRes.value) return Result.err({ message: "streams.traces is required when include.trace is true" });
|
|
217
|
+
|
|
218
|
+
const limitsRaw = isPlainObject(raw.limits) ? raw.limits : {};
|
|
219
|
+
const eventLimitRes = parseLimit(limitsRaw.events, 100, 500, "limits.events");
|
|
220
|
+
if (Result.isError(eventLimitRes)) return eventLimitRes;
|
|
221
|
+
const spanLimitRes = parseLimit(limitsRaw.spans, 5000, 10_000, "limits.spans");
|
|
222
|
+
if (Result.isError(spanLimitRes)) return spanLimitRes;
|
|
223
|
+
const timeRes = parseTime(raw.time);
|
|
224
|
+
if (Result.isError(timeRes)) return timeRes;
|
|
225
|
+
|
|
226
|
+
return Result.ok({
|
|
227
|
+
streams: {
|
|
228
|
+
events: eventsStreamRes.value ?? undefined,
|
|
229
|
+
traces: tracesStreamRes.value ?? undefined,
|
|
230
|
+
},
|
|
231
|
+
lookup: {
|
|
232
|
+
requestId: requestIdRes.value,
|
|
233
|
+
traceId: traceIdRes.value,
|
|
234
|
+
spanId: spanIdRes.value,
|
|
235
|
+
},
|
|
236
|
+
time: timeRes.value,
|
|
237
|
+
include: {
|
|
238
|
+
events: includeEventsRes.value,
|
|
239
|
+
trace: includeTraceRes.value,
|
|
240
|
+
timeline: includeTimelineRes.value,
|
|
241
|
+
raw: includeRawRes.value,
|
|
242
|
+
},
|
|
243
|
+
limits: {
|
|
244
|
+
events: eventLimitRes.value,
|
|
245
|
+
spans: spanLimitRes.value,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function quoteSearchValue(value: string): string {
|
|
251
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function buildTimeSearchClauses(time: ObserveRequestInput["time"]): string[] {
|
|
255
|
+
const out: string[] = [];
|
|
256
|
+
if (time.from) {
|
|
257
|
+
const from = new Date(Date.parse(time.from) - time.paddingMs).toISOString();
|
|
258
|
+
out.push(`timestamp:>=${quoteSearchValue(from)}`);
|
|
259
|
+
}
|
|
260
|
+
if (time.to) {
|
|
261
|
+
const to = new Date(Date.parse(time.to) + time.paddingMs).toISOString();
|
|
262
|
+
out.push(`timestamp:<=${quoteSearchValue(to)}`);
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function combineSearchClauses(...clauses: Array<string | null | undefined>): string {
|
|
268
|
+
return clauses.filter((clause): clause is string => !!clause && clause.trim() !== "").join(" ");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function statusCode(record: Record<string, unknown>): "unset" | "ok" | "error" {
|
|
272
|
+
const status = nestedObject(record, "status");
|
|
273
|
+
const code = stringField(status, "code");
|
|
274
|
+
return code === "ok" || code === "error" ? code : "unset";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function spanIsError(record: Record<string, unknown>): boolean {
|
|
278
|
+
if (statusCode(record) === "error") return true;
|
|
279
|
+
const error = nestedObject(record, "error");
|
|
280
|
+
return error.isError === true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function toTraceNode(record: Record<string, unknown>, depth: number): TraceTreeNode {
|
|
284
|
+
return {
|
|
285
|
+
spanId: stringField(record, "spanId") ?? "",
|
|
286
|
+
parentSpanId: stringField(record, "parentSpanId"),
|
|
287
|
+
children: [],
|
|
288
|
+
depth,
|
|
289
|
+
service: stringField(record, "service"),
|
|
290
|
+
name: stringField(record, "name") ?? "",
|
|
291
|
+
kind: stringField(record, "kind") ?? "unspecified",
|
|
292
|
+
startTime: stringField(record, "timestamp") ?? "",
|
|
293
|
+
endTime: stringField(record, "endTimestamp"),
|
|
294
|
+
duration: numberField(record, "duration"),
|
|
295
|
+
statusCode: statusCode(record),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function compareSpans(left: Record<string, unknown>, right: Record<string, unknown>): number {
|
|
300
|
+
const leftTs = stringField(left, "timestamp") ?? "";
|
|
301
|
+
const rightTs = stringField(right, "timestamp") ?? "";
|
|
302
|
+
if (leftTs !== rightTs) return leftTs < rightTs ? -1 : 1;
|
|
303
|
+
const leftDuration = numberField(left, "duration") ?? -1;
|
|
304
|
+
const rightDuration = numberField(right, "duration") ?? -1;
|
|
305
|
+
if (leftDuration !== rightDuration) return rightDuration - leftDuration;
|
|
306
|
+
return (stringField(left, "name") ?? "").localeCompare(stringField(right, "name") ?? "");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function sortTree(nodes: TraceTreeNode[]): void {
|
|
310
|
+
nodes.sort((left, right) => {
|
|
311
|
+
if (left.startTime !== right.startTime) return left.startTime < right.startTime ? -1 : 1;
|
|
312
|
+
if ((left.duration ?? -1) !== (right.duration ?? -1)) return (right.duration ?? -1) - (left.duration ?? -1);
|
|
313
|
+
return left.name.localeCompare(right.name);
|
|
314
|
+
});
|
|
315
|
+
for (const node of nodes) sortTree(node.children);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function cloneNodeAtDepth(node: TraceTreeNode, depth: number): TraceTreeNode {
|
|
319
|
+
return {
|
|
320
|
+
...node,
|
|
321
|
+
depth,
|
|
322
|
+
children: node.children.map((child) => cloneNodeAtDepth(child, depth + 1)),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function parseTimeMs(value: string | null): number | null {
|
|
327
|
+
if (!value) return null;
|
|
328
|
+
const parsed = Date.parse(value);
|
|
329
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function intervalDurationMs(node: TraceTreeNode): number | null {
|
|
333
|
+
const start = parseTimeMs(node.startTime);
|
|
334
|
+
const end = parseTimeMs(node.endTime);
|
|
335
|
+
if (start == null || end == null || end < start) return node.duration;
|
|
336
|
+
return end - start;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function exclusiveDurationMs(node: TraceTreeNode): number {
|
|
340
|
+
const total = intervalDurationMs(node) ?? node.duration ?? 0;
|
|
341
|
+
const start = parseTimeMs(node.startTime);
|
|
342
|
+
const end = parseTimeMs(node.endTime);
|
|
343
|
+
if (start == null || end == null || end <= start || node.children.length === 0) return Math.max(0, total);
|
|
344
|
+
const intervals = node.children
|
|
345
|
+
.map((child) => {
|
|
346
|
+
const childStart = parseTimeMs(child.startTime);
|
|
347
|
+
const childEnd = parseTimeMs(child.endTime);
|
|
348
|
+
if (childStart == null || childEnd == null || childEnd <= childStart) return null;
|
|
349
|
+
return [Math.max(start, childStart), Math.min(end, childEnd)] as const;
|
|
350
|
+
})
|
|
351
|
+
.filter((interval): interval is readonly [number, number] => !!interval && interval[1] > interval[0])
|
|
352
|
+
.sort((left, right) => left[0] - right[0]);
|
|
353
|
+
let covered = 0;
|
|
354
|
+
let currentStart: number | null = null;
|
|
355
|
+
let currentEnd: number | null = null;
|
|
356
|
+
for (const [left, right] of intervals) {
|
|
357
|
+
if (currentStart == null || currentEnd == null) {
|
|
358
|
+
currentStart = left;
|
|
359
|
+
currentEnd = right;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (left <= currentEnd) {
|
|
363
|
+
currentEnd = Math.max(currentEnd, right);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
covered += currentEnd - currentStart;
|
|
367
|
+
currentStart = left;
|
|
368
|
+
currentEnd = right;
|
|
369
|
+
}
|
|
370
|
+
if (currentStart != null && currentEnd != null) covered += currentEnd - currentStart;
|
|
371
|
+
return Math.max(0, total - covered);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function criticalPathScore(node: TraceTreeNode, memo: Map<string, number>): number {
|
|
375
|
+
const cached = memo.get(node.spanId);
|
|
376
|
+
if (cached != null) return cached;
|
|
377
|
+
const score =
|
|
378
|
+
node.children.length === 0
|
|
379
|
+
? exclusiveDurationMs(node)
|
|
380
|
+
: exclusiveDurationMs(node) + Math.max(...node.children.map((child) => criticalPathScore(child, memo)));
|
|
381
|
+
memo.set(node.spanId, score);
|
|
382
|
+
return score;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function rootSelectionScore(node: TraceTreeNode, record: Record<string, unknown> | undefined): number {
|
|
386
|
+
const http = record ? nestedObject(record, "http") : {};
|
|
387
|
+
const hasHttp =
|
|
388
|
+
stringField(http, "method") != null ||
|
|
389
|
+
stringField(http, "route") != null ||
|
|
390
|
+
stringField(http, "path") != null ||
|
|
391
|
+
numberField(http, "statusCode") != null;
|
|
392
|
+
return (
|
|
393
|
+
(node.parentSpanId == null ? 10_000 : 0) +
|
|
394
|
+
(node.kind === "server" ? 2_000 : 0) +
|
|
395
|
+
(hasHttp ? 1_000 : 0) +
|
|
396
|
+
(record && stringField(record, "requestId") ? 500 : 0) +
|
|
397
|
+
Math.min(node.duration ?? 0, 60_000) / 10
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function selectRootSpanId(rootNodes: TraceTreeNode[], bySpanId: Map<string, Record<string, unknown>>): string | null {
|
|
402
|
+
if (rootNodes.length === 0) return null;
|
|
403
|
+
return [...rootNodes]
|
|
404
|
+
.sort((left, right) => {
|
|
405
|
+
const scoreDiff = rootSelectionScore(right, bySpanId.get(right.spanId)) - rootSelectionScore(left, bySpanId.get(left.spanId));
|
|
406
|
+
if (scoreDiff !== 0) return scoreDiff;
|
|
407
|
+
if (left.startTime !== right.startTime) return left.startTime < right.startTime ? -1 : 1;
|
|
408
|
+
return left.spanId.localeCompare(right.spanId);
|
|
409
|
+
})[0]?.spanId ?? null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function buildCriticalPath(rootNodes: TraceTreeNode[], rootSpanId: string | null): string[] {
|
|
413
|
+
if (rootNodes.length === 0) return [];
|
|
414
|
+
const memo = new Map<string, number>();
|
|
415
|
+
let current =
|
|
416
|
+
rootNodes.find((node) => node.spanId === rootSpanId) ??
|
|
417
|
+
[...rootNodes].sort((a, b) => criticalPathScore(b, memo) - criticalPathScore(a, memo))[0]!;
|
|
418
|
+
const out: string[] = [];
|
|
419
|
+
while (current) {
|
|
420
|
+
out.push(current.spanId);
|
|
421
|
+
if (current.children.length === 0) break;
|
|
422
|
+
current = [...current.children].sort((a, b) => criticalPathScore(b, memo) - criticalPathScore(a, memo))[0]!;
|
|
423
|
+
}
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function buildServiceMap(spans: Record<string, unknown>[], bySpanId: Map<string, Record<string, unknown>>): ServiceEdge[] {
|
|
428
|
+
const edges = new Map<string, ServiceEdge>();
|
|
429
|
+
for (const span of spans) {
|
|
430
|
+
const parentSpanId = stringField(span, "parentSpanId");
|
|
431
|
+
if (!parentSpanId) continue;
|
|
432
|
+
const parent = bySpanId.get(parentSpanId);
|
|
433
|
+
if (!parent) continue;
|
|
434
|
+
const from = stringField(parent, "service");
|
|
435
|
+
const to = stringField(span, "service");
|
|
436
|
+
if (!from || !to || from === to) continue;
|
|
437
|
+
const key = `${from}\0${to}`;
|
|
438
|
+
let edge = edges.get(key);
|
|
439
|
+
if (!edge) {
|
|
440
|
+
edge = {
|
|
441
|
+
from,
|
|
442
|
+
to,
|
|
443
|
+
count: 0,
|
|
444
|
+
errorCount: 0,
|
|
445
|
+
latency: { count: 0, sum: 0, min: null, max: null },
|
|
446
|
+
};
|
|
447
|
+
edges.set(key, edge);
|
|
448
|
+
}
|
|
449
|
+
edge.count += 1;
|
|
450
|
+
if (spanIsError(span)) edge.errorCount += 1;
|
|
451
|
+
const duration = numberField(span, "duration");
|
|
452
|
+
if (duration != null) {
|
|
453
|
+
edge.latency.count += 1;
|
|
454
|
+
edge.latency.sum += duration;
|
|
455
|
+
edge.latency.min = edge.latency.min == null ? duration : Math.min(edge.latency.min, duration);
|
|
456
|
+
edge.latency.max = edge.latency.max == null ? duration : Math.max(edge.latency.max, duration);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return Array.from(edges.values()).sort((a, b) => `${a.from}:${a.to}`.localeCompare(`${b.from}:${b.to}`));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function buildTraceErrors(spans: Record<string, unknown>[]): TraceError[] {
|
|
463
|
+
const errors: TraceError[] = [];
|
|
464
|
+
for (const span of spans) {
|
|
465
|
+
if (!spanIsError(span)) continue;
|
|
466
|
+
const error = nestedObject(span, "error");
|
|
467
|
+
errors.push({
|
|
468
|
+
spanId: stringField(span, "spanId") ?? "",
|
|
469
|
+
service: stringField(span, "service"),
|
|
470
|
+
name: stringField(span, "name") ?? "",
|
|
471
|
+
time: stringField(span, "timestamp"),
|
|
472
|
+
type: stringField(error, "type"),
|
|
473
|
+
message: stringField(error, "message") ?? (isPlainObject(span.status) ? stringField(span.status, "message") : null),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return errors;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function buildTraceDetails(spansRaw: unknown[], args?: { spanLimitReached?: boolean; coverageComplete?: boolean }): TraceDetails {
|
|
480
|
+
const input = spansRaw.filter(isPlainObject).sort(compareSpans);
|
|
481
|
+
const unique = new Map<string, Record<string, unknown>>();
|
|
482
|
+
let duplicateSpans = 0;
|
|
483
|
+
for (const span of input) {
|
|
484
|
+
const traceId = stringField(span, "traceId");
|
|
485
|
+
const spanId = stringField(span, "spanId");
|
|
486
|
+
if (!traceId || !spanId) continue;
|
|
487
|
+
const key = `${traceId}:${spanId}`;
|
|
488
|
+
if (unique.has(key)) {
|
|
489
|
+
duplicateSpans += 1;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
unique.set(key, span);
|
|
493
|
+
}
|
|
494
|
+
const spans = Array.from(unique.values()).sort(compareSpans);
|
|
495
|
+
const bySpanId = new Map<string, Record<string, unknown>>();
|
|
496
|
+
for (const span of spans) {
|
|
497
|
+
const spanId = stringField(span, "spanId");
|
|
498
|
+
if (spanId) bySpanId.set(spanId, span);
|
|
499
|
+
}
|
|
500
|
+
const nodeBySpanId = new Map<string, TraceTreeNode>();
|
|
501
|
+
for (const span of spans) {
|
|
502
|
+
const spanId = stringField(span, "spanId");
|
|
503
|
+
if (spanId) nodeBySpanId.set(spanId, toTraceNode(span, 0));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const roots: TraceTreeNode[] = [];
|
|
507
|
+
const missingParents = new Set<string>();
|
|
508
|
+
for (const span of spans) {
|
|
509
|
+
const spanId = stringField(span, "spanId");
|
|
510
|
+
if (!spanId) continue;
|
|
511
|
+
const node = nodeBySpanId.get(spanId);
|
|
512
|
+
if (!node) continue;
|
|
513
|
+
const parentSpanId = stringField(span, "parentSpanId");
|
|
514
|
+
if (!parentSpanId) {
|
|
515
|
+
roots.push(node);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const parent = nodeBySpanId.get(parentSpanId);
|
|
519
|
+
if (!parent) {
|
|
520
|
+
missingParents.add(parentSpanId);
|
|
521
|
+
roots.push(node);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
parent.children.push(node);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const setDepth = (node: TraceTreeNode, depth: number): TraceTreeNode => {
|
|
528
|
+
node.depth = depth;
|
|
529
|
+
node.children = node.children.map((child) => setDepth(child, depth + 1));
|
|
530
|
+
return node;
|
|
531
|
+
};
|
|
532
|
+
const tree = roots.map((root) => setDepth(root, 0)).map((root) => cloneNodeAtDepth(root, 0));
|
|
533
|
+
sortTree(tree);
|
|
534
|
+
const rootSpanId = selectRootSpanId(tree, bySpanId);
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
traceId: spans.length > 0 ? stringField(spans[0]!, "traceId") : null,
|
|
538
|
+
rootSpanId,
|
|
539
|
+
spans,
|
|
540
|
+
tree,
|
|
541
|
+
serviceMap: buildServiceMap(spans, bySpanId),
|
|
542
|
+
criticalPath: buildCriticalPath(tree, rootSpanId),
|
|
543
|
+
errors: buildTraceErrors(spans),
|
|
544
|
+
partial: (args?.spanLimitReached ?? false) || args?.coverageComplete === false || missingParents.size > 0,
|
|
545
|
+
missingParents: Array.from(missingParents).sort(),
|
|
546
|
+
duplicateSpans,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export function summarizeSearchQueryCoverage(
|
|
551
|
+
q: string,
|
|
552
|
+
batches: SearchResultBatch[],
|
|
553
|
+
hits: SearchHit[],
|
|
554
|
+
limitReached: boolean
|
|
555
|
+
): ObserveSearchQueryCoverage {
|
|
556
|
+
let complete = batches.length > 0;
|
|
557
|
+
let timedOut = false;
|
|
558
|
+
let totalValue = 0;
|
|
559
|
+
let totalRelation: "eq" | "gte" = "eq";
|
|
560
|
+
for (const batch of batches) {
|
|
561
|
+
complete = complete && batch.coverage.complete;
|
|
562
|
+
timedOut = timedOut || batch.timedOut;
|
|
563
|
+
totalValue = Math.max(totalValue, batch.total.value);
|
|
564
|
+
if (batch.total.relation === "gte") totalRelation = "gte";
|
|
565
|
+
}
|
|
566
|
+
if (batches.length === 0) complete = true;
|
|
567
|
+
return {
|
|
568
|
+
q,
|
|
569
|
+
hits: hits.length,
|
|
570
|
+
total: { value: totalValue, relation: totalRelation },
|
|
571
|
+
pages: batches.length,
|
|
572
|
+
complete: complete && !timedOut && !limitReached,
|
|
573
|
+
timed_out: timedOut,
|
|
574
|
+
limit_reached: limitReached,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export function summarizeSearchCoverage(
|
|
579
|
+
batches: SearchResultBatch[],
|
|
580
|
+
hits: SearchHit[],
|
|
581
|
+
limitReached: boolean,
|
|
582
|
+
queries: ObserveSearchQueryCoverage[] = []
|
|
583
|
+
): ObserveSearchCoverage {
|
|
584
|
+
const families = new Set<string>();
|
|
585
|
+
const uniqueHitKeys = new Set<string>();
|
|
586
|
+
let complete = batches.length > 0;
|
|
587
|
+
let timedOut = false;
|
|
588
|
+
let scannedTailDocs = 0;
|
|
589
|
+
let scannedSegments = 0;
|
|
590
|
+
let possibleMissing = 0;
|
|
591
|
+
let totalRelation: "eq" | "gte" = "eq";
|
|
592
|
+
const batchStreams = new Set(batches.map((batch) => batch.stream));
|
|
593
|
+
const fallbackStream = batchStreams.size === 1 ? Array.from(batchStreams)[0]! : "";
|
|
594
|
+
for (const hit of hits) {
|
|
595
|
+
const stream = typeof (hit as SearchHit & { stream?: unknown }).stream === "string" ? (hit as SearchHit & { stream: string }).stream : fallbackStream;
|
|
596
|
+
uniqueHitKeys.add(`${stream}\0${hit.offset}`);
|
|
597
|
+
}
|
|
598
|
+
for (const batch of batches) {
|
|
599
|
+
complete = complete && batch.coverage.complete;
|
|
600
|
+
timedOut = timedOut || batch.timedOut;
|
|
601
|
+
scannedTailDocs += batch.coverage.scannedTailDocs;
|
|
602
|
+
scannedSegments += batch.coverage.scannedSegments;
|
|
603
|
+
possibleMissing += batch.coverage.possibleMissingEventsUpperBound;
|
|
604
|
+
if (batch.total.relation === "gte") totalRelation = "gte";
|
|
605
|
+
for (const family of batch.coverage.indexFamiliesUsed) families.add(family);
|
|
606
|
+
}
|
|
607
|
+
if (batches.length === 0) complete = true;
|
|
608
|
+
const exactUniqueTotal = !limitReached && !timedOut && complete && totalRelation === "eq";
|
|
609
|
+
return {
|
|
610
|
+
searched: batches.length > 0,
|
|
611
|
+
complete: complete && !timedOut && !limitReached,
|
|
612
|
+
timed_out: timedOut,
|
|
613
|
+
limit_reached: limitReached,
|
|
614
|
+
hits: uniqueHitKeys.size,
|
|
615
|
+
unique_hits: uniqueHitKeys.size,
|
|
616
|
+
query_count: batches.length,
|
|
617
|
+
batch_count: batches.length,
|
|
618
|
+
total: { value: uniqueHitKeys.size, relation: exactUniqueTotal ? "eq" : "gte" },
|
|
619
|
+
index_families_used: Array.from(families).sort(),
|
|
620
|
+
scanned_tail_docs: scannedTailDocs,
|
|
621
|
+
scanned_segments: scannedSegments,
|
|
622
|
+
possible_missing_events_upper_bound: possibleMissing,
|
|
623
|
+
queries,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export function sortTimeline(items: UnifiedTimelineItem[]): UnifiedTimelineItem[] {
|
|
628
|
+
return [...items].sort((left, right) => {
|
|
629
|
+
if (left.time !== right.time) return left.time < right.time ? -1 : 1;
|
|
630
|
+
return left.kind.localeCompare(right.kind);
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function evlogLevel(record: Record<string, unknown>): "debug" | "info" | "warn" | "error" | null {
|
|
635
|
+
const level = stringField(record, "level");
|
|
636
|
+
return level === "debug" || level === "info" || level === "warn" || level === "error" ? level : null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function firstString(...values: Array<string | null>): string | null {
|
|
640
|
+
return values.find((value) => value != null) ?? null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function firstNumber(...values: Array<number | null>): number | null {
|
|
644
|
+
return values.find((value) => value != null) ?? null;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export function buildObserveSummary(args: {
|
|
648
|
+
lookup: ObserveRequestInput["lookup"];
|
|
649
|
+
primaryEvent: Record<string, unknown> | null;
|
|
650
|
+
trace: TraceDetails;
|
|
651
|
+
}): Record<string, unknown> {
|
|
652
|
+
const rootSpan = args.trace.spans.find((span) => stringField(span, "spanId") === args.trace.rootSpanId) ?? args.trace.spans[0] ?? null;
|
|
653
|
+
const event = args.primaryEvent;
|
|
654
|
+
const http = rootSpan ? nestedObject(rootSpan, "http") : {};
|
|
655
|
+
const error = rootSpan ? nestedObject(rootSpan, "error") : {};
|
|
656
|
+
const eventStatus = event ? numberField(event, "status") : null;
|
|
657
|
+
const spanStatus = numberField(http, "statusCode");
|
|
658
|
+
const level = event ? evlogLevel(event) : null;
|
|
659
|
+
const method = firstString(event ? stringField(event, "method") : null, stringField(http, "method"));
|
|
660
|
+
const path = firstString(event ? stringField(event, "path") : null, stringField(http, "path"));
|
|
661
|
+
const route = stringField(http, "route");
|
|
662
|
+
const rootStart = rootSpan ? stringField(rootSpan, "timestamp") : null;
|
|
663
|
+
const rootEnd = rootSpan ? stringField(rootSpan, "endTimestamp") : null;
|
|
664
|
+
const eventMessage = event ? stringField(event, "message") : null;
|
|
665
|
+
const spanName = rootSpan ? stringField(rootSpan, "name") : null;
|
|
666
|
+
return {
|
|
667
|
+
title:
|
|
668
|
+
eventMessage ??
|
|
669
|
+
([method, route ?? path].filter(Boolean).join(" ") || spanName || args.lookup.requestId || args.lookup.traceId || args.lookup.spanId || "request"),
|
|
670
|
+
service: firstString(event ? stringField(event, "service") : null, rootSpan ? stringField(rootSpan, "service") : null),
|
|
671
|
+
environment: firstString(event ? stringField(event, "environment") : null, rootSpan ? stringField(rootSpan, "environment") : null),
|
|
672
|
+
method,
|
|
673
|
+
path,
|
|
674
|
+
route,
|
|
675
|
+
status: firstNumber(eventStatus, spanStatus),
|
|
676
|
+
level,
|
|
677
|
+
duration: firstNumber(event ? numberField(event, "duration") : null, rootSpan ? numberField(rootSpan, "duration") : null),
|
|
678
|
+
startTime: firstString(event ? stringField(event, "timestamp") : null, rootStart),
|
|
679
|
+
endTime: rootEnd,
|
|
680
|
+
error: {
|
|
681
|
+
isError: level === "error" || eventStatus != null && eventStatus >= 500 || args.trace.errors.length > 0 || error.isError === true,
|
|
682
|
+
type: stringField(error, "type"),
|
|
683
|
+
message: firstString(stringField(error, "message"), event ? stringField(event, "message") : null),
|
|
684
|
+
why: event ? stringField(event, "why") : null,
|
|
685
|
+
fix: event ? stringField(event, "fix") : null,
|
|
686
|
+
link: event ? stringField(event, "link") : null,
|
|
687
|
+
},
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export function choosePrimaryEvent(events: SearchHit[], traceId: string | null): SearchHit | null {
|
|
692
|
+
if (events.length === 0) return null;
|
|
693
|
+
if (traceId) {
|
|
694
|
+
const matching = events.find((hit) => isPlainObject(hit.source) && stringField(hit.source, "traceId") === traceId);
|
|
695
|
+
if (matching) return matching;
|
|
696
|
+
}
|
|
697
|
+
return events[0]!;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export function compactEvlogRecord(record: unknown): unknown {
|
|
701
|
+
if (!isPlainObject(record)) return record;
|
|
702
|
+
return pickFields(record, [
|
|
703
|
+
"timestamp",
|
|
704
|
+
"level",
|
|
705
|
+
"service",
|
|
706
|
+
"environment",
|
|
707
|
+
"version",
|
|
708
|
+
"region",
|
|
709
|
+
"requestId",
|
|
710
|
+
"traceId",
|
|
711
|
+
"spanId",
|
|
712
|
+
"method",
|
|
713
|
+
"path",
|
|
714
|
+
"status",
|
|
715
|
+
"duration",
|
|
716
|
+
"message",
|
|
717
|
+
"why",
|
|
718
|
+
"fix",
|
|
719
|
+
"link",
|
|
720
|
+
]);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export function compactTraceSpanRecord(record: unknown): unknown {
|
|
724
|
+
if (!isPlainObject(record)) return record;
|
|
725
|
+
const out = pickFields(record, [
|
|
726
|
+
"schemaVersion",
|
|
727
|
+
"signal",
|
|
728
|
+
"timestamp",
|
|
729
|
+
"endTimestamp",
|
|
730
|
+
"startUnixNano",
|
|
731
|
+
"endUnixNano",
|
|
732
|
+
"duration",
|
|
733
|
+
"traceId",
|
|
734
|
+
"spanId",
|
|
735
|
+
"parentSpanId",
|
|
736
|
+
"name",
|
|
737
|
+
"kind",
|
|
738
|
+
"service",
|
|
739
|
+
"serviceNamespace",
|
|
740
|
+
"serviceInstanceId",
|
|
741
|
+
"environment",
|
|
742
|
+
"version",
|
|
743
|
+
"region",
|
|
744
|
+
"requestId",
|
|
745
|
+
"eventNames",
|
|
746
|
+
"dropped",
|
|
747
|
+
]);
|
|
748
|
+
|
|
749
|
+
const traceFlags = nestedObject(record, "traceFlags");
|
|
750
|
+
if (Object.prototype.hasOwnProperty.call(traceFlags, "sampled")) out.traceFlags = { sampled: traceFlags.sampled };
|
|
751
|
+
|
|
752
|
+
const status = compactNested(record, "status", ["code", "message"]);
|
|
753
|
+
if (status) out.status = status;
|
|
754
|
+
const http = compactNested(record, "http", ["method", "route", "path", "statusCode"]);
|
|
755
|
+
if (http) out.http = http;
|
|
756
|
+
const db = compactNested(record, "db", ["system", "name", "operation"]);
|
|
757
|
+
if (db) out.db = db;
|
|
758
|
+
const rpc = compactNested(record, "rpc", ["system", "service", "method"]);
|
|
759
|
+
if (rpc) out.rpc = rpc;
|
|
760
|
+
const messaging = compactNested(record, "messaging", ["system", "destination", "operation"]);
|
|
761
|
+
if (messaging) out.messaging = messaging;
|
|
762
|
+
const error = compactNested(record, "error", ["isError", "type", "message"]);
|
|
763
|
+
if (error) out.error = error;
|
|
764
|
+
|
|
765
|
+
return out;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
export function compactTimelineItem(item: unknown): unknown {
|
|
769
|
+
if (!isPlainObject(item)) return item;
|
|
770
|
+
const { data: _data, ...rest } = item;
|
|
771
|
+
return rest;
|
|
772
|
+
}
|