flock-core 0.5.0b53__py3-none-any.whl → 0.5.0b54__py3-none-any.whl
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.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/agent.py +6 -2
- flock/components.py +17 -1
- flock/dashboard/service.py +293 -0
- flock/frontend/README.md +86 -0
- flock/frontend/src/components/modules/JsonAttributeRenderer.tsx +140 -0
- flock/frontend/src/components/modules/ModuleWindow.tsx +97 -29
- flock/frontend/src/components/modules/TraceModuleJaeger.tsx +1971 -0
- flock/frontend/src/components/modules/TraceModuleJaegerWrapper.tsx +13 -0
- flock/frontend/src/components/modules/registerModules.ts +10 -0
- flock/frontend/src/components/settings/MultiSelect.tsx +235 -0
- flock/frontend/src/components/settings/SettingsPanel.css +1 -1
- flock/frontend/src/components/settings/TracingSettings.tsx +404 -0
- flock/frontend/src/types/modules.ts +3 -0
- flock/logging/auto_trace.py +159 -0
- flock/logging/telemetry.py +17 -0
- flock/logging/telemetry_exporter/duckdb_exporter.py +216 -0
- flock/logging/telemetry_exporter/file_exporter.py +7 -1
- flock/logging/trace_and_logged.py +263 -14
- flock/orchestrator.py +130 -1
- {flock_core-0.5.0b53.dist-info → flock_core-0.5.0b54.dist-info}/METADATA +187 -18
- {flock_core-0.5.0b53.dist-info → flock_core-0.5.0b54.dist-info}/RECORD +24 -17
- {flock_core-0.5.0b53.dist-info → flock_core-0.5.0b54.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b53.dist-info → flock_core-0.5.0b54.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b53.dist-info → flock_core-0.5.0b54.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1971 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { ModuleContext } from './ModuleRegistry';
|
|
3
|
+
import JsonAttributeRenderer from './JsonAttributeRenderer';
|
|
4
|
+
import TracingSettings from '../settings/TracingSettings';
|
|
5
|
+
|
|
6
|
+
interface Span {
|
|
7
|
+
name: string;
|
|
8
|
+
context: {
|
|
9
|
+
trace_id: string;
|
|
10
|
+
span_id: string;
|
|
11
|
+
};
|
|
12
|
+
parent_id?: string;
|
|
13
|
+
start_time: number;
|
|
14
|
+
end_time: number;
|
|
15
|
+
status: {
|
|
16
|
+
status_code: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
};
|
|
19
|
+
attributes: Record<string, string>;
|
|
20
|
+
kind: string;
|
|
21
|
+
resource: {
|
|
22
|
+
'service.name'?: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface SpanNode extends Span {
|
|
27
|
+
children: SpanNode[];
|
|
28
|
+
depth: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface TraceModuleJaegerProps {
|
|
32
|
+
context: ModuleContext;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface TraceGroup {
|
|
36
|
+
traceId: string;
|
|
37
|
+
spans: Span[];
|
|
38
|
+
startTime: number;
|
|
39
|
+
endTime: number;
|
|
40
|
+
duration: number;
|
|
41
|
+
spanCount: number;
|
|
42
|
+
hasError: boolean;
|
|
43
|
+
services: Set<string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ServiceMetrics {
|
|
47
|
+
service: string;
|
|
48
|
+
totalSpans: number;
|
|
49
|
+
errorSpans: number;
|
|
50
|
+
avgDuration: number;
|
|
51
|
+
p95Duration: number;
|
|
52
|
+
p99Duration: number;
|
|
53
|
+
rate: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface OperationMetrics {
|
|
57
|
+
operation: string;
|
|
58
|
+
service: string;
|
|
59
|
+
totalCalls: number;
|
|
60
|
+
errorCalls: number;
|
|
61
|
+
avgDuration: number;
|
|
62
|
+
p95Duration: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface DependencyEdge {
|
|
66
|
+
from: string; // parent service
|
|
67
|
+
to: string; // child service
|
|
68
|
+
operations: Map<string, OperationMetrics>; // parent.operation -> child.operation
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type ViewMode = 'timeline' | 'statistics' | 'metrics' | 'dependencies' | 'sql' | 'configuration' | 'guide';
|
|
72
|
+
|
|
73
|
+
const TraceModuleJaeger: React.FC<TraceModuleJaegerProps> = () => {
|
|
74
|
+
const [traces, setTraces] = useState<Span[]>([]);
|
|
75
|
+
const [loading, setLoading] = useState(true);
|
|
76
|
+
const [error, setError] = useState<string | null>(null);
|
|
77
|
+
const [selectedTraceIds, setSelectedTraceIds] = useState<Set<string>>(new Set());
|
|
78
|
+
const [expandedSpans, setExpandedSpans] = useState<Set<string>>(new Set());
|
|
79
|
+
const [collapsedSpans, setCollapsedSpans] = useState<Set<string>>(new Set());
|
|
80
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
81
|
+
const [viewMode, setViewMode] = useState<ViewMode>('timeline');
|
|
82
|
+
const [focusedSpanId, setFocusedSpanId] = useState<string | null>(null);
|
|
83
|
+
const [expandedDeps, setExpandedDeps] = useState<Set<string>>(new Set());
|
|
84
|
+
|
|
85
|
+
// SQL query state
|
|
86
|
+
const [sqlQuery, setSqlQuery] = useState('SELECT * FROM spans LIMIT 10');
|
|
87
|
+
const [sqlResults, setSqlResults] = useState<any[] | null>(null);
|
|
88
|
+
const [sqlColumns, setSqlColumns] = useState<string[]>([]);
|
|
89
|
+
const [sqlLoading, setSqlLoading] = useState(false);
|
|
90
|
+
const [sqlError, setSqlError] = useState<string | null>(null);
|
|
91
|
+
|
|
92
|
+
// Sort state for timeline/statistics
|
|
93
|
+
type SortField = 'date' | 'spans' | 'duration';
|
|
94
|
+
type SortOrder = 'asc' | 'desc';
|
|
95
|
+
const [sortField, setSortField] = useState<SortField>('date');
|
|
96
|
+
const [sortOrder, setSortOrder] = useState<SortOrder>('desc'); // newest first by default
|
|
97
|
+
|
|
98
|
+
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
99
|
+
const lastTraceCountRef = useRef<number>(0);
|
|
100
|
+
|
|
101
|
+
// Execute SQL query
|
|
102
|
+
const executeSqlQuery = async () => {
|
|
103
|
+
setSqlLoading(true);
|
|
104
|
+
setSqlError(null);
|
|
105
|
+
try {
|
|
106
|
+
const response = await fetch('/api/traces/query', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: { 'Content-Type': 'application/json' },
|
|
109
|
+
body: JSON.stringify({ query: sqlQuery }),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
const error = await response.json();
|
|
114
|
+
throw new Error(error.error || 'Query failed');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const data = await response.json();
|
|
118
|
+
setSqlResults(data.results);
|
|
119
|
+
setSqlColumns(data.columns);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
setSqlError(err instanceof Error ? err.message : 'Unknown error');
|
|
122
|
+
setSqlResults(null);
|
|
123
|
+
setSqlColumns([]);
|
|
124
|
+
} finally {
|
|
125
|
+
setSqlLoading(false);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Service colors - assign consistent colors per service or span type
|
|
130
|
+
const serviceColors = useMemo(() => {
|
|
131
|
+
const colors = [
|
|
132
|
+
'#3b82f6', // blue
|
|
133
|
+
'#10b981', // green
|
|
134
|
+
'#f59e0b', // amber
|
|
135
|
+
'#ef4444', // red
|
|
136
|
+
'#8b5cf6', // purple
|
|
137
|
+
'#ec4899', // pink
|
|
138
|
+
'#06b6d4', // cyan
|
|
139
|
+
'#f97316', // orange
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
const colorMap = new Map<string, string>();
|
|
143
|
+
const services: string[] = [];
|
|
144
|
+
const spanTypes: string[] = [];
|
|
145
|
+
|
|
146
|
+
traces.forEach(span => {
|
|
147
|
+
// Extract service from span name (e.g., "Flock.publish" -> service: "Flock")
|
|
148
|
+
const serviceName = span.name.split('.')[0] || span.resource['service.name'] || 'unknown';
|
|
149
|
+
if (serviceName && !services.includes(serviceName)) {
|
|
150
|
+
services.push(serviceName);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Also track span types for color coding (use full span name for more granular types)
|
|
154
|
+
const spanType = span.name.split('.')[0] || span.name; // Get class name
|
|
155
|
+
if (spanType && !spanTypes.includes(spanType)) {
|
|
156
|
+
spanTypes.push(spanType);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// If all spans have the same service, color by span type instead
|
|
161
|
+
if (services.length === 1) {
|
|
162
|
+
spanTypes.forEach((type, idx) => {
|
|
163
|
+
const color = colors[idx % colors.length] || '#6366f1';
|
|
164
|
+
colorMap.set(type, color);
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
services.forEach((service, idx) => {
|
|
168
|
+
const color = colors[idx % colors.length] || '#6366f1';
|
|
169
|
+
colorMap.set(service!, color);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { colorMap, useSpanType: services.length === 1 };
|
|
174
|
+
}, [traces]);
|
|
175
|
+
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
const fetchTraces = async () => {
|
|
178
|
+
try {
|
|
179
|
+
if (traces.length === 0) {
|
|
180
|
+
setLoading(true);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const response = await fetch('/api/traces');
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
186
|
+
}
|
|
187
|
+
const data = await response.json();
|
|
188
|
+
|
|
189
|
+
if (JSON.stringify(data) !== JSON.stringify(traces)) {
|
|
190
|
+
const scrollTop = scrollContainerRef.current?.scrollTop || 0;
|
|
191
|
+
setTraces(data);
|
|
192
|
+
setError(null);
|
|
193
|
+
|
|
194
|
+
requestAnimationFrame(() => {
|
|
195
|
+
if (scrollContainerRef.current && data.length === lastTraceCountRef.current) {
|
|
196
|
+
scrollContainerRef.current.scrollTop = scrollTop;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
lastTraceCountRef.current = data.length;
|
|
201
|
+
}
|
|
202
|
+
} catch (err) {
|
|
203
|
+
setError(err instanceof Error ? err.message : 'Failed to load traces');
|
|
204
|
+
} finally {
|
|
205
|
+
setLoading(false);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
fetchTraces();
|
|
210
|
+
const interval = setInterval(fetchTraces, 5000);
|
|
211
|
+
return () => clearInterval(interval);
|
|
212
|
+
}, [traces]);
|
|
213
|
+
|
|
214
|
+
const traceGroups = useMemo((): TraceGroup[] => {
|
|
215
|
+
const grouped = new Map<string, Span[]>();
|
|
216
|
+
|
|
217
|
+
traces.forEach(span => {
|
|
218
|
+
const traceId = span.context.trace_id;
|
|
219
|
+
if (!grouped.has(traceId)) {
|
|
220
|
+
grouped.set(traceId, []);
|
|
221
|
+
}
|
|
222
|
+
grouped.get(traceId)!.push(span);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return Array.from(grouped.entries()).map(([traceId, spans]) => {
|
|
226
|
+
const startTime = Math.min(...spans.map(s => s.start_time));
|
|
227
|
+
const endTime = Math.max(...spans.map(s => s.end_time));
|
|
228
|
+
const duration = (endTime - startTime) / 1_000_000;
|
|
229
|
+
const hasError = spans.some(s => s.status.status_code === 'ERROR');
|
|
230
|
+
const services = new Set(spans.map(s => s.name.split('.')[0] || s.resource['service.name'] || 'unknown'));
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
traceId,
|
|
234
|
+
spans: spans.sort((a, b) => a.start_time - b.start_time),
|
|
235
|
+
startTime,
|
|
236
|
+
endTime,
|
|
237
|
+
duration,
|
|
238
|
+
spanCount: spans.length,
|
|
239
|
+
hasError,
|
|
240
|
+
services,
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
}, [traces]);
|
|
244
|
+
|
|
245
|
+
const filteredTraces = useMemo(() => {
|
|
246
|
+
let result = traceGroups;
|
|
247
|
+
|
|
248
|
+
if (searchQuery) {
|
|
249
|
+
const query = searchQuery.toLowerCase();
|
|
250
|
+
result = result.filter(trace =>
|
|
251
|
+
trace.traceId.toLowerCase().includes(query) ||
|
|
252
|
+
trace.spans.some(span =>
|
|
253
|
+
span.name.toLowerCase().includes(query) ||
|
|
254
|
+
Object.values(span.attributes).some(val =>
|
|
255
|
+
typeof val === 'string' && val.toLowerCase().includes(query)
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Apply sorting
|
|
262
|
+
return [...result].sort((a, b) => {
|
|
263
|
+
let comparison = 0;
|
|
264
|
+
|
|
265
|
+
switch (sortField) {
|
|
266
|
+
case 'date':
|
|
267
|
+
comparison = a.startTime - b.startTime;
|
|
268
|
+
break;
|
|
269
|
+
case 'spans':
|
|
270
|
+
comparison = a.spanCount - b.spanCount;
|
|
271
|
+
break;
|
|
272
|
+
case 'duration':
|
|
273
|
+
comparison = a.duration - b.duration;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return sortOrder === 'asc' ? comparison : -comparison;
|
|
278
|
+
});
|
|
279
|
+
}, [traceGroups, searchQuery, sortField, sortOrder]);
|
|
280
|
+
|
|
281
|
+
// Calculate RED metrics per service
|
|
282
|
+
const serviceMetrics = useMemo<ServiceMetrics[]>(() => {
|
|
283
|
+
const metricsMap = new Map<string, ServiceMetrics>();
|
|
284
|
+
|
|
285
|
+
traces.forEach(span => {
|
|
286
|
+
const service = span.name.split('.')[0] || 'unknown';
|
|
287
|
+
if (!metricsMap.has(service)) {
|
|
288
|
+
metricsMap.set(service, {
|
|
289
|
+
service,
|
|
290
|
+
totalSpans: 0,
|
|
291
|
+
errorSpans: 0,
|
|
292
|
+
avgDuration: 0,
|
|
293
|
+
p95Duration: 0,
|
|
294
|
+
p99Duration: 0,
|
|
295
|
+
rate: 0,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const metrics = metricsMap.get(service)!;
|
|
300
|
+
metrics.totalSpans++;
|
|
301
|
+
if (span.status.status_code === 'ERROR') {
|
|
302
|
+
metrics.errorSpans++;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Calculate durations and rate
|
|
307
|
+
metricsMap.forEach((metrics, service) => {
|
|
308
|
+
const serviceSpans = traces.filter(s => (s.name.split('.')[0] || 'unknown') === service);
|
|
309
|
+
const durations = serviceSpans.map(s => (s.end_time - s.start_time) / 1_000_000).sort((a, b) => a - b);
|
|
310
|
+
|
|
311
|
+
metrics.avgDuration = durations.reduce((sum, d) => sum + d, 0) / durations.length || 0;
|
|
312
|
+
metrics.p95Duration = durations[Math.floor(durations.length * 0.95)] || 0;
|
|
313
|
+
metrics.p99Duration = durations[Math.floor(durations.length * 0.99)] || 0;
|
|
314
|
+
|
|
315
|
+
// Calculate rate (spans per second)
|
|
316
|
+
if (serviceSpans.length > 1) {
|
|
317
|
+
const timeSpan = (Math.max(...serviceSpans.map(s => s.end_time)) -
|
|
318
|
+
Math.min(...serviceSpans.map(s => s.start_time))) / 1_000_000_000;
|
|
319
|
+
metrics.rate = serviceSpans.length / Math.max(timeSpan, 1);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
return Array.from(metricsMap.values()).sort((a, b) => b.totalSpans - a.totalSpans);
|
|
324
|
+
}, [traces]);
|
|
325
|
+
|
|
326
|
+
// Build service dependency graph with operation-level drill-down
|
|
327
|
+
const serviceDependencies = useMemo(() => {
|
|
328
|
+
const deps = new Map<string, DependencyEdge>();
|
|
329
|
+
|
|
330
|
+
traceGroups.forEach(trace => {
|
|
331
|
+
trace.spans.forEach(span => {
|
|
332
|
+
if (span.parent_id) {
|
|
333
|
+
const parent = trace.spans.find(s => s.context.span_id === span.parent_id);
|
|
334
|
+
if (parent) {
|
|
335
|
+
const parentService = parent.name.split('.')[0] || 'unknown';
|
|
336
|
+
const childService = span.name.split('.')[0] || 'unknown';
|
|
337
|
+
|
|
338
|
+
// Only track cross-service dependencies
|
|
339
|
+
if (parentService !== childService) {
|
|
340
|
+
const key = `${parentService}->${childService}`;
|
|
341
|
+
|
|
342
|
+
if (!deps.has(key)) {
|
|
343
|
+
deps.set(key, {
|
|
344
|
+
from: parentService,
|
|
345
|
+
to: childService,
|
|
346
|
+
operations: new Map(),
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const edge = deps.get(key)!;
|
|
351
|
+
const opKey = `${parent.name} → ${span.name}`;
|
|
352
|
+
|
|
353
|
+
if (!edge.operations.has(opKey)) {
|
|
354
|
+
edge.operations.set(opKey, {
|
|
355
|
+
operation: opKey,
|
|
356
|
+
service: childService,
|
|
357
|
+
totalCalls: 0,
|
|
358
|
+
errorCalls: 0,
|
|
359
|
+
avgDuration: 0,
|
|
360
|
+
p95Duration: 0,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const opMetrics = edge.operations.get(opKey)!;
|
|
365
|
+
opMetrics.totalCalls++;
|
|
366
|
+
if (span.status.status_code === 'ERROR') {
|
|
367
|
+
opMetrics.errorCalls++;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Calculate operation metrics
|
|
376
|
+
deps.forEach(edge => {
|
|
377
|
+
edge.operations.forEach((opMetrics, opKey) => {
|
|
378
|
+
const childOp = opKey.split(' → ')[1];
|
|
379
|
+
const relevantSpans = traces.filter(s => s.name === childOp);
|
|
380
|
+
const durations = relevantSpans.map(s => (s.end_time - s.start_time) / 1_000_000).sort((a, b) => a - b);
|
|
381
|
+
|
|
382
|
+
opMetrics.avgDuration = durations.reduce((sum, d) => sum + d, 0) / durations.length || 0;
|
|
383
|
+
opMetrics.p95Duration = durations[Math.floor(durations.length * 0.95)] || 0;
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
return Array.from(deps.values());
|
|
388
|
+
}, [traceGroups, traces]);
|
|
389
|
+
|
|
390
|
+
const buildSpanTree = (spans: Span[]): SpanNode[] => {
|
|
391
|
+
const spanMap = new Map<string, SpanNode>();
|
|
392
|
+
const roots: SpanNode[] = [];
|
|
393
|
+
|
|
394
|
+
spans.forEach(span => {
|
|
395
|
+
spanMap.set(span.context.span_id, {
|
|
396
|
+
...span,
|
|
397
|
+
children: [],
|
|
398
|
+
depth: 0,
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
spans.forEach(span => {
|
|
403
|
+
const node = spanMap.get(span.context.span_id)!;
|
|
404
|
+
|
|
405
|
+
if (span.parent_id && spanMap.has(span.parent_id)) {
|
|
406
|
+
const parent = spanMap.get(span.parent_id)!;
|
|
407
|
+
parent.children.push(node);
|
|
408
|
+
node.depth = parent.depth + 1;
|
|
409
|
+
} else {
|
|
410
|
+
roots.push(node);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const sortChildren = (node: SpanNode) => {
|
|
415
|
+
node.children.sort((a, b) => a.start_time - b.start_time);
|
|
416
|
+
node.children.forEach(sortChildren);
|
|
417
|
+
};
|
|
418
|
+
roots.forEach(sortChildren);
|
|
419
|
+
|
|
420
|
+
return roots;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const toggleSpanExpand = (spanId: string) => {
|
|
424
|
+
setExpandedSpans(prev => {
|
|
425
|
+
const newSet = new Set(prev);
|
|
426
|
+
if (newSet.has(spanId)) {
|
|
427
|
+
newSet.delete(spanId);
|
|
428
|
+
} else {
|
|
429
|
+
newSet.add(spanId);
|
|
430
|
+
}
|
|
431
|
+
return newSet;
|
|
432
|
+
});
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const toggleSpanCollapse = (spanId: string) => {
|
|
436
|
+
setCollapsedSpans(prev => {
|
|
437
|
+
const newSet = new Set(prev);
|
|
438
|
+
if (newSet.has(spanId)) {
|
|
439
|
+
newSet.delete(spanId);
|
|
440
|
+
} else {
|
|
441
|
+
newSet.add(spanId);
|
|
442
|
+
}
|
|
443
|
+
return newSet;
|
|
444
|
+
});
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const getServiceColor = (serviceName: string | undefined, spanName: string): string => {
|
|
448
|
+
if (serviceColors.useSpanType) {
|
|
449
|
+
// Color by span type if all services are the same
|
|
450
|
+
const spanType = spanName.split('.')[0] || spanName;
|
|
451
|
+
return serviceColors.colorMap.get(spanType) || '#6366f1';
|
|
452
|
+
}
|
|
453
|
+
if (!serviceName) return '#6366f1';
|
|
454
|
+
return serviceColors.colorMap.get(serviceName) || '#6366f1';
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const renderStatisticsView = (trace: TraceGroup) => {
|
|
458
|
+
return (
|
|
459
|
+
<div style={{
|
|
460
|
+
marginTop: 'var(--space-component-md)',
|
|
461
|
+
background: 'var(--color-bg-base)',
|
|
462
|
+
borderRadius: 'var(--radius-lg)',
|
|
463
|
+
overflow: 'hidden',
|
|
464
|
+
}}>
|
|
465
|
+
<table style={{
|
|
466
|
+
width: '100%',
|
|
467
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
468
|
+
fontFamily: 'var(--font-family-mono)',
|
|
469
|
+
borderCollapse: 'collapse',
|
|
470
|
+
}}>
|
|
471
|
+
<thead>
|
|
472
|
+
<tr style={{
|
|
473
|
+
background: 'var(--color-bg-surface)',
|
|
474
|
+
borderBottom: '2px solid var(--color-border-subtle)',
|
|
475
|
+
}}>
|
|
476
|
+
<th style={{ padding: '12px', textAlign: 'left', color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-bold)' }}>Service</th>
|
|
477
|
+
<th style={{ padding: '12px', textAlign: 'left', color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-bold)' }}>Operation</th>
|
|
478
|
+
<th style={{ padding: '12px', textAlign: 'right', color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-bold)' }}>Duration</th>
|
|
479
|
+
<th style={{ padding: '12px', textAlign: 'right', color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-bold)' }}>Start Time</th>
|
|
480
|
+
<th style={{ padding: '12px', textAlign: 'center', color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-bold)' }}>Status</th>
|
|
481
|
+
</tr>
|
|
482
|
+
</thead>
|
|
483
|
+
<tbody>
|
|
484
|
+
{trace.spans.map((span, idx) => {
|
|
485
|
+
const duration = (span.end_time - span.start_time) / 1_000_000;
|
|
486
|
+
const startOffset = (span.start_time - trace.startTime) / 1_000_000;
|
|
487
|
+
const serviceName = span.name.split('.')[0] || span.resource['service.name'] || 'unknown';
|
|
488
|
+
|
|
489
|
+
return (
|
|
490
|
+
<tr
|
|
491
|
+
key={span.context.span_id}
|
|
492
|
+
style={{
|
|
493
|
+
background: idx % 2 === 0 ? 'transparent' : 'var(--color-bg-surface)',
|
|
494
|
+
borderBottom: '1px solid var(--color-border-subtle)',
|
|
495
|
+
}}
|
|
496
|
+
>
|
|
497
|
+
<td style={{ padding: '10px' }}>
|
|
498
|
+
<span style={{
|
|
499
|
+
display: 'inline-block',
|
|
500
|
+
width: '8px',
|
|
501
|
+
height: '8px',
|
|
502
|
+
borderRadius: '50%',
|
|
503
|
+
background: getServiceColor(serviceName, span.name),
|
|
504
|
+
marginRight: '8px',
|
|
505
|
+
}} />
|
|
506
|
+
{serviceName}
|
|
507
|
+
</td>
|
|
508
|
+
<td style={{ padding: '10px', color: 'var(--color-text-primary)' }}>{span.name}</td>
|
|
509
|
+
<td style={{ padding: '10px', textAlign: 'right', color: 'var(--color-text-primary)', fontWeight: 'var(--font-weight-medium)' }}>
|
|
510
|
+
{duration.toFixed(2)}ms
|
|
511
|
+
</td>
|
|
512
|
+
<td style={{ padding: '10px', textAlign: 'right', color: 'var(--color-text-tertiary)' }}>
|
|
513
|
+
+{startOffset.toFixed(2)}ms
|
|
514
|
+
</td>
|
|
515
|
+
<td style={{ padding: '10px', textAlign: 'center' }}>
|
|
516
|
+
<span style={{
|
|
517
|
+
padding: '2px 8px',
|
|
518
|
+
borderRadius: 'var(--radius-sm)',
|
|
519
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
520
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
521
|
+
background: span.status.status_code === 'ERROR' ? 'rgba(239, 68, 68, 0.2)' : 'rgba(16, 185, 129, 0.2)',
|
|
522
|
+
color: span.status.status_code === 'ERROR' ? '#ef4444' : '#10b981',
|
|
523
|
+
}}>
|
|
524
|
+
{span.status.status_code}
|
|
525
|
+
</span>
|
|
526
|
+
</td>
|
|
527
|
+
</tr>
|
|
528
|
+
);
|
|
529
|
+
})}
|
|
530
|
+
</tbody>
|
|
531
|
+
</table>
|
|
532
|
+
</div>
|
|
533
|
+
);
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const renderSpanNode = (
|
|
537
|
+
node: SpanNode,
|
|
538
|
+
traceStartTime: number,
|
|
539
|
+
scaleDuration: number
|
|
540
|
+
): React.ReactElement => {
|
|
541
|
+
const spanStartOffset = (node.start_time - traceStartTime) / 1_000_000;
|
|
542
|
+
const spanDuration = (node.end_time - node.start_time) / 1_000_000;
|
|
543
|
+
|
|
544
|
+
const leftPercent = Math.min((spanStartOffset / scaleDuration) * 100, 100);
|
|
545
|
+
const widthPercent = Math.min((spanDuration / scaleDuration) * 100, 100 - leftPercent);
|
|
546
|
+
const displayWidthPercent = Math.max(widthPercent, 0.5);
|
|
547
|
+
|
|
548
|
+
const isExpanded = expandedSpans.has(node.context.span_id);
|
|
549
|
+
const isCollapsed = collapsedSpans.has(node.context.span_id);
|
|
550
|
+
const hasChildren = node.children.length > 0;
|
|
551
|
+
const serviceName = node.name.split('.')[0] || node.resource['service.name'] || 'unknown';
|
|
552
|
+
const serviceColor = getServiceColor(serviceName, node.name);
|
|
553
|
+
const isFocused = focusedSpanId === node.context.span_id;
|
|
554
|
+
|
|
555
|
+
return (
|
|
556
|
+
<div key={node.context.span_id} style={{ marginBottom: '1px', opacity: isFocused ? 1 : (focusedSpanId ? 0.4 : 1) }}>
|
|
557
|
+
<div
|
|
558
|
+
style={{
|
|
559
|
+
display: 'grid',
|
|
560
|
+
gridTemplateColumns: '400px 1fr',
|
|
561
|
+
alignItems: 'center',
|
|
562
|
+
background: isExpanded ? 'var(--color-bg-elevated)' : 'transparent',
|
|
563
|
+
borderBottom: '1px solid rgba(255, 255, 255, 0.03)',
|
|
564
|
+
}}
|
|
565
|
+
>
|
|
566
|
+
{/* Left side: Hierarchy */}
|
|
567
|
+
<div style={{
|
|
568
|
+
padding: '8px 12px',
|
|
569
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
570
|
+
fontFamily: 'var(--font-family-mono)',
|
|
571
|
+
color: 'var(--color-text-primary)',
|
|
572
|
+
display: 'flex',
|
|
573
|
+
alignItems: 'center',
|
|
574
|
+
gap: '8px',
|
|
575
|
+
paddingLeft: `${12 + node.depth * 20}px`,
|
|
576
|
+
borderRight: '1px solid var(--color-border-subtle)',
|
|
577
|
+
}}>
|
|
578
|
+
{hasChildren && (
|
|
579
|
+
<span
|
|
580
|
+
onClick={() => toggleSpanCollapse(node.context.span_id)}
|
|
581
|
+
style={{
|
|
582
|
+
cursor: 'pointer',
|
|
583
|
+
userSelect: 'none',
|
|
584
|
+
width: '12px',
|
|
585
|
+
opacity: 0.6,
|
|
586
|
+
fontSize: '10px',
|
|
587
|
+
}}
|
|
588
|
+
>
|
|
589
|
+
{isCollapsed ? '►' : '▼'}
|
|
590
|
+
</span>
|
|
591
|
+
)}
|
|
592
|
+
{!hasChildren && <span style={{ width: '12px' }} />}
|
|
593
|
+
|
|
594
|
+
<span
|
|
595
|
+
style={{
|
|
596
|
+
display: 'inline-block',
|
|
597
|
+
width: '6px',
|
|
598
|
+
height: '6px',
|
|
599
|
+
borderRadius: '50%',
|
|
600
|
+
background: serviceColor,
|
|
601
|
+
flexShrink: 0,
|
|
602
|
+
}}
|
|
603
|
+
/>
|
|
604
|
+
|
|
605
|
+
<span
|
|
606
|
+
onClick={(e) => {
|
|
607
|
+
if (e.shiftKey) {
|
|
608
|
+
setFocusedSpanId(isFocused ? null : node.context.span_id);
|
|
609
|
+
} else {
|
|
610
|
+
toggleSpanExpand(node.context.span_id);
|
|
611
|
+
}
|
|
612
|
+
}}
|
|
613
|
+
style={{
|
|
614
|
+
flex: 1,
|
|
615
|
+
overflow: 'hidden',
|
|
616
|
+
textOverflow: 'ellipsis',
|
|
617
|
+
whiteSpace: 'nowrap',
|
|
618
|
+
cursor: 'pointer',
|
|
619
|
+
display: 'flex',
|
|
620
|
+
alignItems: 'center',
|
|
621
|
+
gap: '4px',
|
|
622
|
+
}}
|
|
623
|
+
title={`${node.name}\nShift+click to focus`}
|
|
624
|
+
>
|
|
625
|
+
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }}>{node.name}</span>
|
|
626
|
+
</span>
|
|
627
|
+
|
|
628
|
+
<span style={{
|
|
629
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
630
|
+
color: 'var(--color-text-tertiary)',
|
|
631
|
+
flexShrink: 0,
|
|
632
|
+
}}>
|
|
633
|
+
{spanDuration.toFixed(1)}ms
|
|
634
|
+
</span>
|
|
635
|
+
</div>
|
|
636
|
+
|
|
637
|
+
{/* Right side: Gantt chart */}
|
|
638
|
+
<div style={{
|
|
639
|
+
padding: '8px 12px',
|
|
640
|
+
height: '32px',
|
|
641
|
+
}}>
|
|
642
|
+
<div style={{
|
|
643
|
+
position: 'relative',
|
|
644
|
+
height: '18px',
|
|
645
|
+
width: '100%',
|
|
646
|
+
}}>
|
|
647
|
+
<div
|
|
648
|
+
style={{
|
|
649
|
+
position: 'absolute',
|
|
650
|
+
left: `${leftPercent}%`,
|
|
651
|
+
width: `${displayWidthPercent}%`,
|
|
652
|
+
height: '100%',
|
|
653
|
+
background: serviceColor,
|
|
654
|
+
border: node.status.status_code === 'ERROR' ? '2px solid #ef4444' : 'none',
|
|
655
|
+
borderRadius: '2px',
|
|
656
|
+
display: 'flex',
|
|
657
|
+
alignItems: 'center',
|
|
658
|
+
paddingLeft: '4px',
|
|
659
|
+
fontSize: '10px',
|
|
660
|
+
color: 'white',
|
|
661
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
662
|
+
cursor: 'pointer',
|
|
663
|
+
boxSizing: 'border-box',
|
|
664
|
+
}}
|
|
665
|
+
onClick={() => toggleSpanExpand(node.context.span_id)}
|
|
666
|
+
title={`${node.name}\nService: ${serviceName || 'unknown'}\n${spanDuration.toFixed(2)}ms\nStart: +${spanStartOffset.toFixed(2)}ms`}
|
|
667
|
+
/>
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
|
|
672
|
+
{isExpanded && (
|
|
673
|
+
<div style={{
|
|
674
|
+
background: 'var(--color-bg-surface)',
|
|
675
|
+
border: '1px solid var(--color-border-subtle)',
|
|
676
|
+
borderLeft: `4px solid ${serviceColor}`,
|
|
677
|
+
margin: '0 12px 8px 12px',
|
|
678
|
+
padding: 'var(--space-component-sm)',
|
|
679
|
+
borderRadius: 'var(--radius-md)',
|
|
680
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
681
|
+
fontFamily: 'var(--font-family-mono)',
|
|
682
|
+
}}>
|
|
683
|
+
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: '6px', color: 'var(--color-text-secondary)' }}>
|
|
684
|
+
<div style={{ color: 'var(--color-text-tertiary)', fontWeight: 'var(--font-weight-medium)' }}>Service:</div>
|
|
685
|
+
<div>{serviceName}</div>
|
|
686
|
+
|
|
687
|
+
<div style={{ color: 'var(--color-text-tertiary)', fontWeight: 'var(--font-weight-medium)' }}>Span ID:</div>
|
|
688
|
+
<div style={{ wordBreak: 'break-all' }}>{node.context.span_id}</div>
|
|
689
|
+
|
|
690
|
+
{node.parent_id && (
|
|
691
|
+
<>
|
|
692
|
+
<div style={{ color: 'var(--color-text-tertiary)', fontWeight: 'var(--font-weight-medium)' }}>Parent ID:</div>
|
|
693
|
+
<div style={{ wordBreak: 'break-all' }}>{node.parent_id}</div>
|
|
694
|
+
</>
|
|
695
|
+
)}
|
|
696
|
+
|
|
697
|
+
<div style={{ color: 'var(--color-text-tertiary)', fontWeight: 'var(--font-weight-medium)' }}>Duration:</div>
|
|
698
|
+
<div>{spanDuration.toFixed(3)}ms</div>
|
|
699
|
+
|
|
700
|
+
<div style={{ color: 'var(--color-text-tertiary)', fontWeight: 'var(--font-weight-medium)' }}>Start Time:</div>
|
|
701
|
+
<div>+{spanStartOffset.toFixed(3)}ms</div>
|
|
702
|
+
|
|
703
|
+
<div style={{ color: 'var(--color-text-tertiary)', fontWeight: 'var(--font-weight-medium)' }}>Status:</div>
|
|
704
|
+
<div style={{
|
|
705
|
+
color: node.status.status_code === 'ERROR' ? '#ef4444' : '#10b981',
|
|
706
|
+
fontWeight: 'var(--font-weight-bold)',
|
|
707
|
+
}}>
|
|
708
|
+
{node.status.status_code}
|
|
709
|
+
</div>
|
|
710
|
+
|
|
711
|
+
{Object.entries(node.attributes).length > 0 && (
|
|
712
|
+
<>
|
|
713
|
+
<div style={{
|
|
714
|
+
gridColumn: '1 / -1',
|
|
715
|
+
borderTop: '1px solid var(--color-border-subtle)',
|
|
716
|
+
margin: '8px 0 4px 0',
|
|
717
|
+
paddingTop: '8px',
|
|
718
|
+
color: 'var(--color-text-secondary)',
|
|
719
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
720
|
+
}}>
|
|
721
|
+
Tags:
|
|
722
|
+
</div>
|
|
723
|
+
{Object.entries(node.attributes).map(([key, value]) => (
|
|
724
|
+
<React.Fragment key={key}>
|
|
725
|
+
<div style={{ color: 'var(--color-text-tertiary)', alignSelf: 'start' }}>{key}:</div>
|
|
726
|
+
<div>
|
|
727
|
+
<JsonAttributeRenderer value={value} />
|
|
728
|
+
</div>
|
|
729
|
+
</React.Fragment>
|
|
730
|
+
))}
|
|
731
|
+
</>
|
|
732
|
+
)}
|
|
733
|
+
</div>
|
|
734
|
+
</div>
|
|
735
|
+
)}
|
|
736
|
+
|
|
737
|
+
{hasChildren && !isCollapsed && (
|
|
738
|
+
<div>
|
|
739
|
+
{node.children.map(child => renderSpanNode(child, traceStartTime, scaleDuration))}
|
|
740
|
+
</div>
|
|
741
|
+
)}
|
|
742
|
+
</div>
|
|
743
|
+
);
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
const renderTimelineView = (trace: TraceGroup) => {
|
|
747
|
+
const maxEndOffset = Math.max(...trace.spans.map(s => (s.end_time - trace.startTime) / 1_000_000));
|
|
748
|
+
const scaleDuration = Math.max(maxEndOffset, trace.duration);
|
|
749
|
+
const spanTree = buildSpanTree(trace.spans);
|
|
750
|
+
|
|
751
|
+
return (
|
|
752
|
+
<div style={{
|
|
753
|
+
marginTop: 'var(--space-component-md)',
|
|
754
|
+
background: 'var(--color-bg-base)',
|
|
755
|
+
borderRadius: 'var(--radius-lg)',
|
|
756
|
+
overflow: 'hidden',
|
|
757
|
+
}}>
|
|
758
|
+
{/* Header with timeline scale */}
|
|
759
|
+
<div style={{
|
|
760
|
+
display: 'grid',
|
|
761
|
+
gridTemplateColumns: '400px 1fr',
|
|
762
|
+
background: 'var(--color-bg-surface)',
|
|
763
|
+
borderBottom: '2px solid var(--color-border-subtle)',
|
|
764
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
765
|
+
color: 'var(--color-text-tertiary)',
|
|
766
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
767
|
+
}}>
|
|
768
|
+
<div style={{ padding: '10px 12px', borderRight: '1px solid var(--color-border-subtle)' }}>
|
|
769
|
+
Service & Operation
|
|
770
|
+
</div>
|
|
771
|
+
<div style={{ padding: '10px 12px' }}>
|
|
772
|
+
Timeline (0ms - {scaleDuration.toFixed(0)}ms)
|
|
773
|
+
</div>
|
|
774
|
+
</div>
|
|
775
|
+
|
|
776
|
+
{spanTree.map(node => renderSpanNode(node, trace.startTime, scaleDuration))}
|
|
777
|
+
</div>
|
|
778
|
+
);
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
if (loading) {
|
|
782
|
+
return (
|
|
783
|
+
<div style={{
|
|
784
|
+
display: 'flex',
|
|
785
|
+
alignItems: 'center',
|
|
786
|
+
justifyContent: 'center',
|
|
787
|
+
height: '100%',
|
|
788
|
+
color: 'var(--color-text-secondary)',
|
|
789
|
+
}}>
|
|
790
|
+
<div style={{ textAlign: 'center' }}>
|
|
791
|
+
<div style={{ fontSize: '32px', marginBottom: 'var(--gap-md)', opacity: 0.5 }}>🔎</div>
|
|
792
|
+
<div>Loading traces...</div>
|
|
793
|
+
</div>
|
|
794
|
+
</div>
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (error) {
|
|
799
|
+
return (
|
|
800
|
+
<div style={{
|
|
801
|
+
display: 'flex',
|
|
802
|
+
alignItems: 'center',
|
|
803
|
+
justifyContent: 'center',
|
|
804
|
+
height: '100%',
|
|
805
|
+
color: 'var(--color-error)',
|
|
806
|
+
}}>
|
|
807
|
+
<div style={{ textAlign: 'center' }}>
|
|
808
|
+
<div style={{ fontSize: '32px', marginBottom: 'var(--gap-md)' }}>⚠️</div>
|
|
809
|
+
<div>{error}</div>
|
|
810
|
+
</div>
|
|
811
|
+
</div>
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return (
|
|
816
|
+
<div style={{
|
|
817
|
+
height: '100%',
|
|
818
|
+
display: 'flex',
|
|
819
|
+
flexDirection: 'column',
|
|
820
|
+
background: 'var(--color-bg-surface)',
|
|
821
|
+
}}>
|
|
822
|
+
{/* Two-row header */}
|
|
823
|
+
<div style={{
|
|
824
|
+
padding: 'var(--space-component-md)',
|
|
825
|
+
borderBottom: '1px solid var(--color-border-subtle)',
|
|
826
|
+
display: 'flex',
|
|
827
|
+
flexDirection: 'column',
|
|
828
|
+
gap: 'var(--gap-sm)',
|
|
829
|
+
}}>
|
|
830
|
+
{/* Row 1: View mode buttons */}
|
|
831
|
+
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
832
|
+
<button
|
|
833
|
+
onClick={() => setViewMode('timeline')}
|
|
834
|
+
style={{
|
|
835
|
+
padding: '6px 12px',
|
|
836
|
+
border: '1px solid var(--color-border-subtle)',
|
|
837
|
+
borderRadius: 'var(--radius-md)',
|
|
838
|
+
background: viewMode === 'timeline' ? 'var(--color-primary-500)' : 'var(--color-bg-base)',
|
|
839
|
+
color: viewMode === 'timeline' ? 'white' : 'var(--color-text-primary)',
|
|
840
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
841
|
+
cursor: 'pointer',
|
|
842
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
843
|
+
}}
|
|
844
|
+
>
|
|
845
|
+
📅 Timeline
|
|
846
|
+
</button>
|
|
847
|
+
<button
|
|
848
|
+
onClick={() => setViewMode('statistics')}
|
|
849
|
+
style={{
|
|
850
|
+
padding: '6px 12px',
|
|
851
|
+
border: '1px solid var(--color-border-subtle)',
|
|
852
|
+
borderRadius: 'var(--radius-md)',
|
|
853
|
+
background: viewMode === 'statistics' ? 'var(--color-primary-500)' : 'var(--color-bg-base)',
|
|
854
|
+
color: viewMode === 'statistics' ? 'white' : 'var(--color-text-primary)',
|
|
855
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
856
|
+
cursor: 'pointer',
|
|
857
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
858
|
+
}}
|
|
859
|
+
>
|
|
860
|
+
📊 Statistics
|
|
861
|
+
</button>
|
|
862
|
+
<button
|
|
863
|
+
onClick={() => setViewMode('metrics')}
|
|
864
|
+
style={{
|
|
865
|
+
padding: '6px 12px',
|
|
866
|
+
border: '1px solid var(--color-border-subtle)',
|
|
867
|
+
borderRadius: 'var(--radius-md)',
|
|
868
|
+
background: viewMode === 'metrics' ? 'var(--color-primary-500)' : 'var(--color-bg-base)',
|
|
869
|
+
color: viewMode === 'metrics' ? 'white' : 'var(--color-text-primary)',
|
|
870
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
871
|
+
cursor: 'pointer',
|
|
872
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
873
|
+
}}
|
|
874
|
+
>
|
|
875
|
+
🔴 RED Metrics
|
|
876
|
+
</button>
|
|
877
|
+
<button
|
|
878
|
+
onClick={() => setViewMode('dependencies')}
|
|
879
|
+
style={{
|
|
880
|
+
padding: '6px 12px',
|
|
881
|
+
border: '1px solid var(--color-border-subtle)',
|
|
882
|
+
borderRadius: 'var(--radius-md)',
|
|
883
|
+
background: viewMode === 'dependencies' ? 'var(--color-primary-500)' : 'var(--color-bg-base)',
|
|
884
|
+
color: viewMode === 'dependencies' ? 'white' : 'var(--color-text-primary)',
|
|
885
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
886
|
+
cursor: 'pointer',
|
|
887
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
888
|
+
}}
|
|
889
|
+
>
|
|
890
|
+
🔗 Dependencies
|
|
891
|
+
</button>
|
|
892
|
+
<button
|
|
893
|
+
onClick={() => setViewMode('sql')}
|
|
894
|
+
style={{
|
|
895
|
+
padding: '6px 12px',
|
|
896
|
+
border: '1px solid var(--color-border-subtle)',
|
|
897
|
+
borderRadius: 'var(--radius-md)',
|
|
898
|
+
background: viewMode === 'sql' ? 'var(--color-primary-500)' : 'var(--color-bg-base)',
|
|
899
|
+
color: viewMode === 'sql' ? 'white' : 'var(--color-text-primary)',
|
|
900
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
901
|
+
cursor: 'pointer',
|
|
902
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
903
|
+
}}
|
|
904
|
+
>
|
|
905
|
+
🗄️ DuckDB SQL
|
|
906
|
+
</button>
|
|
907
|
+
<button
|
|
908
|
+
onClick={() => setViewMode('configuration')}
|
|
909
|
+
style={{
|
|
910
|
+
padding: '6px 12px',
|
|
911
|
+
border: '1px solid var(--color-border-subtle)',
|
|
912
|
+
borderRadius: 'var(--radius-md)',
|
|
913
|
+
background: viewMode === 'configuration' ? 'var(--color-primary-500)' : 'var(--color-bg-base)',
|
|
914
|
+
color: viewMode === 'configuration' ? 'white' : 'var(--color-text-primary)',
|
|
915
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
916
|
+
cursor: 'pointer',
|
|
917
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
918
|
+
}}
|
|
919
|
+
>
|
|
920
|
+
⚙️ Configuration
|
|
921
|
+
</button>
|
|
922
|
+
<button
|
|
923
|
+
onClick={() => setViewMode('guide')}
|
|
924
|
+
style={{
|
|
925
|
+
padding: '6px 12px',
|
|
926
|
+
border: '1px solid var(--color-border-subtle)',
|
|
927
|
+
borderRadius: 'var(--radius-md)',
|
|
928
|
+
background: viewMode === 'guide' ? 'var(--color-primary-500)' : 'var(--color-bg-base)',
|
|
929
|
+
color: viewMode === 'guide' ? 'white' : 'var(--color-text-primary)',
|
|
930
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
931
|
+
cursor: 'pointer',
|
|
932
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
933
|
+
}}
|
|
934
|
+
>
|
|
935
|
+
📚 Guide
|
|
936
|
+
</button>
|
|
937
|
+
|
|
938
|
+
{/* Trace count */}
|
|
939
|
+
{(viewMode === 'timeline' || viewMode === 'statistics') && (
|
|
940
|
+
<div style={{
|
|
941
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
942
|
+
color: 'var(--color-text-secondary)',
|
|
943
|
+
marginLeft: 'auto',
|
|
944
|
+
}}>
|
|
945
|
+
{filteredTraces.length} trace{filteredTraces.length !== 1 ? 's' : ''}
|
|
946
|
+
</div>
|
|
947
|
+
)}
|
|
948
|
+
</div>
|
|
949
|
+
|
|
950
|
+
{/* Row 2: Search box */}
|
|
951
|
+
<input
|
|
952
|
+
type="text"
|
|
953
|
+
placeholder="🔎 Find traces (Jaeger style) - Search by service, operation, trace ID, correlation ID, or error"
|
|
954
|
+
value={searchQuery}
|
|
955
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
956
|
+
style={{
|
|
957
|
+
width: '100%',
|
|
958
|
+
padding: 'var(--space-component-sm)',
|
|
959
|
+
border: '1px solid var(--color-border-subtle)',
|
|
960
|
+
borderRadius: 'var(--radius-md)',
|
|
961
|
+
background: 'var(--color-bg-base)',
|
|
962
|
+
color: 'var(--color-text-primary)',
|
|
963
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
964
|
+
}}
|
|
965
|
+
/>
|
|
966
|
+
|
|
967
|
+
{/* Sort buttons for Timeline and Statistics */}
|
|
968
|
+
{(viewMode === 'timeline' || viewMode === 'statistics') && (
|
|
969
|
+
<div style={{
|
|
970
|
+
display: 'flex',
|
|
971
|
+
gap: 'var(--gap-xs)',
|
|
972
|
+
alignItems: 'center',
|
|
973
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
974
|
+
}}>
|
|
975
|
+
<span style={{ color: 'var(--color-text-tertiary)', flexShrink: 0 }}>Sort:</span>
|
|
976
|
+
{(['date', 'spans', 'duration'] as SortField[]).map((field) => {
|
|
977
|
+
const isActive = sortField === field;
|
|
978
|
+
const labels = { date: 'Date', spans: 'Spans', duration: 'Duration' };
|
|
979
|
+
|
|
980
|
+
return (
|
|
981
|
+
<button
|
|
982
|
+
key={field}
|
|
983
|
+
onClick={() => {
|
|
984
|
+
if (sortField === field) {
|
|
985
|
+
// Toggle order if same field
|
|
986
|
+
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
|
987
|
+
} else {
|
|
988
|
+
// Switch field, use default order
|
|
989
|
+
setSortField(field);
|
|
990
|
+
setSortOrder(field === 'date' ? 'desc' : 'asc');
|
|
991
|
+
}
|
|
992
|
+
}}
|
|
993
|
+
style={{
|
|
994
|
+
padding: '4px 10px',
|
|
995
|
+
background: isActive ? 'var(--color-accent)' : 'var(--color-bg-elevated)',
|
|
996
|
+
color: isActive ? 'white' : 'var(--color-text-secondary)',
|
|
997
|
+
border: isActive ? 'none' : '1px solid var(--color-border-subtle)',
|
|
998
|
+
borderRadius: 'var(--radius-sm)',
|
|
999
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1000
|
+
cursor: 'pointer',
|
|
1001
|
+
display: 'flex',
|
|
1002
|
+
alignItems: 'center',
|
|
1003
|
+
gap: '4px',
|
|
1004
|
+
fontWeight: isActive ? 'var(--font-weight-medium)' : 'normal',
|
|
1005
|
+
}}
|
|
1006
|
+
>
|
|
1007
|
+
<span>{labels[field]}</span>
|
|
1008
|
+
{isActive && (
|
|
1009
|
+
<span style={{ fontSize: '10px' }}>
|
|
1010
|
+
{sortOrder === 'asc' ? '↑' : '↓'}
|
|
1011
|
+
</span>
|
|
1012
|
+
)}
|
|
1013
|
+
</button>
|
|
1014
|
+
);
|
|
1015
|
+
})}
|
|
1016
|
+
</div>
|
|
1017
|
+
)}
|
|
1018
|
+
</div>
|
|
1019
|
+
|
|
1020
|
+
<div
|
|
1021
|
+
ref={scrollContainerRef}
|
|
1022
|
+
style={{ flex: 1, overflow: 'auto', padding: 'var(--space-component-md)' }}
|
|
1023
|
+
>
|
|
1024
|
+
{/* RED Metrics View */}
|
|
1025
|
+
{viewMode === 'metrics' && (
|
|
1026
|
+
<div>
|
|
1027
|
+
<div style={{
|
|
1028
|
+
fontSize: 'var(--font-size-body-md)',
|
|
1029
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
1030
|
+
color: 'var(--color-text-primary)',
|
|
1031
|
+
marginBottom: 'var(--space-component-md)',
|
|
1032
|
+
}}>
|
|
1033
|
+
RED Metrics by Service
|
|
1034
|
+
</div>
|
|
1035
|
+
{serviceMetrics.length === 0 ? (
|
|
1036
|
+
<div style={{ textAlign: 'center', padding: 'var(--space-component-xl)', color: 'var(--color-text-tertiary)' }}>
|
|
1037
|
+
No metrics data available
|
|
1038
|
+
</div>
|
|
1039
|
+
) : (
|
|
1040
|
+
<div style={{ display: 'grid', gap: 'var(--gap-md)' }}>
|
|
1041
|
+
{serviceMetrics.map(metrics => {
|
|
1042
|
+
const serviceColor = serviceColors.colorMap.get(metrics.service) || '#6366f1';
|
|
1043
|
+
const errorRate = metrics.totalSpans > 0 ? (metrics.errorSpans / metrics.totalSpans) * 100 : 0;
|
|
1044
|
+
|
|
1045
|
+
return (
|
|
1046
|
+
<div
|
|
1047
|
+
key={metrics.service}
|
|
1048
|
+
style={{
|
|
1049
|
+
padding: 'var(--space-component-md)',
|
|
1050
|
+
background: 'var(--color-bg-elevated)',
|
|
1051
|
+
borderRadius: 'var(--radius-lg)',
|
|
1052
|
+
border: '1px solid var(--color-border-subtle)',
|
|
1053
|
+
borderLeft: `4px solid ${serviceColor}`,
|
|
1054
|
+
}}
|
|
1055
|
+
>
|
|
1056
|
+
<div style={{
|
|
1057
|
+
fontSize: 'var(--font-size-body-lg)',
|
|
1058
|
+
fontWeight: 'var(--font-weight-bold)',
|
|
1059
|
+
color: serviceColor,
|
|
1060
|
+
marginBottom: 'var(--space-component-sm)',
|
|
1061
|
+
}}>
|
|
1062
|
+
{metrics.service}
|
|
1063
|
+
</div>
|
|
1064
|
+
|
|
1065
|
+
<div style={{
|
|
1066
|
+
display: 'grid',
|
|
1067
|
+
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
|
|
1068
|
+
gap: 'var(--gap-md)',
|
|
1069
|
+
}}>
|
|
1070
|
+
<div>
|
|
1071
|
+
<div style={{ fontSize: 'var(--font-size-body-xs)', color: 'var(--color-text-tertiary)', marginBottom: '4px' }}>
|
|
1072
|
+
Rate
|
|
1073
|
+
</div>
|
|
1074
|
+
<div style={{ fontSize: 'var(--font-size-body-lg)', fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)' }}>
|
|
1075
|
+
{metrics.rate.toFixed(2)} req/s
|
|
1076
|
+
</div>
|
|
1077
|
+
</div>
|
|
1078
|
+
|
|
1079
|
+
<div>
|
|
1080
|
+
<div style={{ fontSize: 'var(--font-size-body-xs)', color: 'var(--color-text-tertiary)', marginBottom: '4px' }}>
|
|
1081
|
+
Error Rate
|
|
1082
|
+
</div>
|
|
1083
|
+
<div style={{
|
|
1084
|
+
fontSize: 'var(--font-size-body-lg)',
|
|
1085
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
1086
|
+
color: errorRate > 0 ? '#ef4444' : '#10b981',
|
|
1087
|
+
}}>
|
|
1088
|
+
{errorRate.toFixed(1)}%
|
|
1089
|
+
</div>
|
|
1090
|
+
</div>
|
|
1091
|
+
|
|
1092
|
+
<div>
|
|
1093
|
+
<div style={{ fontSize: 'var(--font-size-body-xs)', color: 'var(--color-text-tertiary)', marginBottom: '4px' }}>
|
|
1094
|
+
Avg Duration
|
|
1095
|
+
</div>
|
|
1096
|
+
<div style={{ fontSize: 'var(--font-size-body-lg)', fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)' }}>
|
|
1097
|
+
{metrics.avgDuration.toFixed(2)}ms
|
|
1098
|
+
</div>
|
|
1099
|
+
</div>
|
|
1100
|
+
|
|
1101
|
+
<div>
|
|
1102
|
+
<div style={{ fontSize: 'var(--font-size-body-xs)', color: 'var(--color-text-tertiary)', marginBottom: '4px' }}>
|
|
1103
|
+
P95 Duration
|
|
1104
|
+
</div>
|
|
1105
|
+
<div style={{ fontSize: 'var(--font-size-body-lg)', fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)' }}>
|
|
1106
|
+
{metrics.p95Duration.toFixed(2)}ms
|
|
1107
|
+
</div>
|
|
1108
|
+
</div>
|
|
1109
|
+
|
|
1110
|
+
<div>
|
|
1111
|
+
<div style={{ fontSize: 'var(--font-size-body-xs)', color: 'var(--color-text-tertiary)', marginBottom: '4px' }}>
|
|
1112
|
+
P99 Duration
|
|
1113
|
+
</div>
|
|
1114
|
+
<div style={{ fontSize: 'var(--font-size-body-lg)', fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)' }}>
|
|
1115
|
+
{metrics.p99Duration.toFixed(2)}ms
|
|
1116
|
+
</div>
|
|
1117
|
+
</div>
|
|
1118
|
+
|
|
1119
|
+
<div>
|
|
1120
|
+
<div style={{ fontSize: 'var(--font-size-body-xs)', color: 'var(--color-text-tertiary)', marginBottom: '4px' }}>
|
|
1121
|
+
Total Spans
|
|
1122
|
+
</div>
|
|
1123
|
+
<div style={{ fontSize: 'var(--font-size-body-lg)', fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)' }}>
|
|
1124
|
+
{metrics.totalSpans}
|
|
1125
|
+
</div>
|
|
1126
|
+
</div>
|
|
1127
|
+
</div>
|
|
1128
|
+
</div>
|
|
1129
|
+
);
|
|
1130
|
+
})}
|
|
1131
|
+
</div>
|
|
1132
|
+
)}
|
|
1133
|
+
</div>
|
|
1134
|
+
)}
|
|
1135
|
+
|
|
1136
|
+
{/* Dependencies View */}
|
|
1137
|
+
{viewMode === 'dependencies' && (
|
|
1138
|
+
<div>
|
|
1139
|
+
<div style={{
|
|
1140
|
+
fontSize: 'var(--font-size-body-md)',
|
|
1141
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
1142
|
+
color: 'var(--color-text-primary)',
|
|
1143
|
+
marginBottom: 'var(--space-component-md)',
|
|
1144
|
+
}}>
|
|
1145
|
+
Service Dependencies with Operation Drill-down
|
|
1146
|
+
</div>
|
|
1147
|
+
{serviceDependencies.length === 0 ? (
|
|
1148
|
+
<div style={{ textAlign: 'center', padding: 'var(--space-component-xl)', color: 'var(--color-text-tertiary)' }}>
|
|
1149
|
+
No service dependencies detected
|
|
1150
|
+
</div>
|
|
1151
|
+
) : (
|
|
1152
|
+
<div style={{ display: 'grid', gap: 'var(--gap-md)' }}>
|
|
1153
|
+
{serviceDependencies.map(dep => {
|
|
1154
|
+
const depKey = `${dep.from}->${dep.to}`;
|
|
1155
|
+
const isExpanded = expandedDeps.has(depKey);
|
|
1156
|
+
const fromColor = serviceColors.colorMap.get(dep.from) || '#6366f1';
|
|
1157
|
+
const toColor = serviceColors.colorMap.get(dep.to) || '#6366f1';
|
|
1158
|
+
|
|
1159
|
+
return (
|
|
1160
|
+
<div
|
|
1161
|
+
key={depKey}
|
|
1162
|
+
style={{
|
|
1163
|
+
padding: 'var(--space-component-md)',
|
|
1164
|
+
background: 'var(--color-bg-elevated)',
|
|
1165
|
+
borderRadius: 'var(--radius-lg)',
|
|
1166
|
+
border: '1px solid var(--color-border-subtle)',
|
|
1167
|
+
}}
|
|
1168
|
+
>
|
|
1169
|
+
<div
|
|
1170
|
+
onClick={() => {
|
|
1171
|
+
setExpandedDeps(prev => {
|
|
1172
|
+
const newSet = new Set(prev);
|
|
1173
|
+
if (newSet.has(depKey)) {
|
|
1174
|
+
newSet.delete(depKey);
|
|
1175
|
+
} else {
|
|
1176
|
+
newSet.add(depKey);
|
|
1177
|
+
}
|
|
1178
|
+
return newSet;
|
|
1179
|
+
});
|
|
1180
|
+
}}
|
|
1181
|
+
style={{
|
|
1182
|
+
display: 'flex',
|
|
1183
|
+
alignItems: 'center',
|
|
1184
|
+
gap: 'var(--gap-md)',
|
|
1185
|
+
cursor: 'pointer',
|
|
1186
|
+
marginBottom: isExpanded ? 'var(--space-component-md)' : 0,
|
|
1187
|
+
}}
|
|
1188
|
+
>
|
|
1189
|
+
<div style={{ fontSize: '14px', opacity: 0.5 }}>
|
|
1190
|
+
{isExpanded ? '▼' : '►'}
|
|
1191
|
+
</div>
|
|
1192
|
+
|
|
1193
|
+
<div
|
|
1194
|
+
style={{
|
|
1195
|
+
padding: '6px 16px',
|
|
1196
|
+
borderRadius: 'var(--radius-md)',
|
|
1197
|
+
background: fromColor,
|
|
1198
|
+
color: 'white',
|
|
1199
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
1200
|
+
fontWeight: 'var(--font-weight-bold)',
|
|
1201
|
+
}}
|
|
1202
|
+
>
|
|
1203
|
+
{dep.from}
|
|
1204
|
+
</div>
|
|
1205
|
+
|
|
1206
|
+
<div style={{ fontSize: 'var(--font-size-body-lg)', color: 'var(--color-text-tertiary)' }}>
|
|
1207
|
+
→
|
|
1208
|
+
</div>
|
|
1209
|
+
|
|
1210
|
+
<div
|
|
1211
|
+
style={{
|
|
1212
|
+
padding: '6px 16px',
|
|
1213
|
+
borderRadius: 'var(--radius-md)',
|
|
1214
|
+
background: toColor,
|
|
1215
|
+
color: 'white',
|
|
1216
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
1217
|
+
fontWeight: 'var(--font-weight-bold)',
|
|
1218
|
+
}}
|
|
1219
|
+
>
|
|
1220
|
+
{dep.to}
|
|
1221
|
+
</div>
|
|
1222
|
+
|
|
1223
|
+
<div style={{
|
|
1224
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1225
|
+
color: 'var(--color-text-tertiary)',
|
|
1226
|
+
marginLeft: 'auto',
|
|
1227
|
+
}}>
|
|
1228
|
+
{dep.operations.size} operation{dep.operations.size !== 1 ? 's' : ''}
|
|
1229
|
+
</div>
|
|
1230
|
+
</div>
|
|
1231
|
+
|
|
1232
|
+
{/* Operation-level drill-down */}
|
|
1233
|
+
{isExpanded && (
|
|
1234
|
+
<div style={{
|
|
1235
|
+
display: 'grid',
|
|
1236
|
+
gap: 'var(--gap-sm)',
|
|
1237
|
+
paddingLeft: '30px',
|
|
1238
|
+
}}>
|
|
1239
|
+
{Array.from(dep.operations.values()).map(opMetrics => {
|
|
1240
|
+
const errorRate = opMetrics.totalCalls > 0 ? (opMetrics.errorCalls / opMetrics.totalCalls) * 100 : 0;
|
|
1241
|
+
|
|
1242
|
+
return (
|
|
1243
|
+
<div
|
|
1244
|
+
key={opMetrics.operation}
|
|
1245
|
+
style={{
|
|
1246
|
+
padding: 'var(--space-component-sm)',
|
|
1247
|
+
background: 'var(--color-bg-surface)',
|
|
1248
|
+
borderRadius: 'var(--radius-md)',
|
|
1249
|
+
border: '1px solid var(--color-border-subtle)',
|
|
1250
|
+
}}
|
|
1251
|
+
>
|
|
1252
|
+
<div style={{
|
|
1253
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1254
|
+
fontFamily: 'var(--font-family-mono)',
|
|
1255
|
+
color: 'var(--color-text-secondary)',
|
|
1256
|
+
marginBottom: 'var(--gap-xs)',
|
|
1257
|
+
}}>
|
|
1258
|
+
{opMetrics.operation}
|
|
1259
|
+
</div>
|
|
1260
|
+
|
|
1261
|
+
<div style={{
|
|
1262
|
+
display: 'flex',
|
|
1263
|
+
gap: 'var(--gap-lg)',
|
|
1264
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1265
|
+
}}>
|
|
1266
|
+
<div>
|
|
1267
|
+
<span style={{ color: 'var(--color-text-tertiary)' }}>Calls: </span>
|
|
1268
|
+
<span style={{ color: 'var(--color-text-primary)', fontWeight: 'var(--font-weight-medium)' }}>
|
|
1269
|
+
{opMetrics.totalCalls}
|
|
1270
|
+
</span>
|
|
1271
|
+
</div>
|
|
1272
|
+
|
|
1273
|
+
<div>
|
|
1274
|
+
<span style={{ color: 'var(--color-text-tertiary)' }}>Errors: </span>
|
|
1275
|
+
<span style={{ color: errorRate > 0 ? '#ef4444' : '#10b981', fontWeight: 'var(--font-weight-medium)' }}>
|
|
1276
|
+
{errorRate.toFixed(1)}%
|
|
1277
|
+
</span>
|
|
1278
|
+
</div>
|
|
1279
|
+
|
|
1280
|
+
<div>
|
|
1281
|
+
<span style={{ color: 'var(--color-text-tertiary)' }}>Avg: </span>
|
|
1282
|
+
<span style={{ color: 'var(--color-text-primary)', fontWeight: 'var(--font-weight-medium)' }}>
|
|
1283
|
+
{opMetrics.avgDuration.toFixed(2)}ms
|
|
1284
|
+
</span>
|
|
1285
|
+
</div>
|
|
1286
|
+
|
|
1287
|
+
<div>
|
|
1288
|
+
<span style={{ color: 'var(--color-text-tertiary)' }}>P95: </span>
|
|
1289
|
+
<span style={{ color: 'var(--color-text-primary)', fontWeight: 'var(--font-weight-medium)' }}>
|
|
1290
|
+
{opMetrics.p95Duration.toFixed(2)}ms
|
|
1291
|
+
</span>
|
|
1292
|
+
</div>
|
|
1293
|
+
</div>
|
|
1294
|
+
</div>
|
|
1295
|
+
);
|
|
1296
|
+
})}
|
|
1297
|
+
</div>
|
|
1298
|
+
)}
|
|
1299
|
+
</div>
|
|
1300
|
+
);
|
|
1301
|
+
})}
|
|
1302
|
+
</div>
|
|
1303
|
+
)}
|
|
1304
|
+
</div>
|
|
1305
|
+
)}
|
|
1306
|
+
|
|
1307
|
+
{/* Configuration View */}
|
|
1308
|
+
{viewMode === 'configuration' && (
|
|
1309
|
+
<div>
|
|
1310
|
+
<TracingSettings />
|
|
1311
|
+
</div>
|
|
1312
|
+
)}
|
|
1313
|
+
|
|
1314
|
+
{/* Guide View */}
|
|
1315
|
+
{viewMode === 'guide' && (
|
|
1316
|
+
<div style={{
|
|
1317
|
+
display: 'flex',
|
|
1318
|
+
flexDirection: 'column',
|
|
1319
|
+
gap: 'var(--gap-lg)',
|
|
1320
|
+
padding: 'var(--space-component-md)',
|
|
1321
|
+
}}>
|
|
1322
|
+
<div>
|
|
1323
|
+
<h3 style={{
|
|
1324
|
+
fontSize: 'var(--font-size-heading-sm)',
|
|
1325
|
+
fontWeight: 'var(--font-weight-bold)',
|
|
1326
|
+
color: 'var(--color-text-primary)',
|
|
1327
|
+
marginBottom: 'var(--gap-md)',
|
|
1328
|
+
}}>
|
|
1329
|
+
🚀 Quick Start Guide
|
|
1330
|
+
</h3>
|
|
1331
|
+
<div style={{
|
|
1332
|
+
display: 'grid',
|
|
1333
|
+
gap: 'var(--gap-md)',
|
|
1334
|
+
color: 'var(--color-text-secondary)',
|
|
1335
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
1336
|
+
}}>
|
|
1337
|
+
<p>
|
|
1338
|
+
The Trace Viewer provides comprehensive observability for your Flock applications using OpenTelemetry distributed tracing.
|
|
1339
|
+
</p>
|
|
1340
|
+
</div>
|
|
1341
|
+
</div>
|
|
1342
|
+
|
|
1343
|
+
<div>
|
|
1344
|
+
<h4 style={{
|
|
1345
|
+
fontSize: 'var(--font-size-body-md)',
|
|
1346
|
+
fontWeight: 'var(--font-weight-bold)',
|
|
1347
|
+
color: 'var(--color-text-primary)',
|
|
1348
|
+
marginBottom: 'var(--gap-sm)',
|
|
1349
|
+
}}>
|
|
1350
|
+
📊 View Modes
|
|
1351
|
+
</h4>
|
|
1352
|
+
<div style={{
|
|
1353
|
+
display: 'grid',
|
|
1354
|
+
gap: 'var(--gap-sm)',
|
|
1355
|
+
}}>
|
|
1356
|
+
{[
|
|
1357
|
+
{ icon: '📅', name: 'Timeline', desc: 'Waterfall view of spans showing execution flow and dependencies' },
|
|
1358
|
+
{ icon: '📈', name: 'Statistics', desc: 'Aggregated metrics including duration, error rates, and call counts' },
|
|
1359
|
+
{ icon: '🔴', name: 'RED Metrics', desc: 'Rate, Errors, Duration metrics for service health monitoring' },
|
|
1360
|
+
{ icon: '🔗', name: 'Dependencies', desc: 'Service-to-service communication patterns and operation drill-down' },
|
|
1361
|
+
{ icon: '🗄️', name: 'DuckDB SQL', desc: 'Write custom SQL queries against the trace database for advanced analysis' },
|
|
1362
|
+
{ icon: '⚙️', name: 'Configuration', desc: 'Configure tracing settings, service filters, and operation blacklists' },
|
|
1363
|
+
].map((mode) => (
|
|
1364
|
+
<div
|
|
1365
|
+
key={mode.name}
|
|
1366
|
+
style={{
|
|
1367
|
+
padding: 'var(--space-component-sm)',
|
|
1368
|
+
background: 'var(--color-bg-surface)',
|
|
1369
|
+
borderRadius: 'var(--radius-md)',
|
|
1370
|
+
border: '1px solid var(--color-border-subtle)',
|
|
1371
|
+
}}
|
|
1372
|
+
>
|
|
1373
|
+
<div style={{ fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)', marginBottom: '4px' }}>
|
|
1374
|
+
{mode.icon} {mode.name}
|
|
1375
|
+
</div>
|
|
1376
|
+
<div style={{ fontSize: 'var(--font-size-body-xs)', color: 'var(--color-text-tertiary)' }}>
|
|
1377
|
+
{mode.desc}
|
|
1378
|
+
</div>
|
|
1379
|
+
</div>
|
|
1380
|
+
))}
|
|
1381
|
+
</div>
|
|
1382
|
+
</div>
|
|
1383
|
+
|
|
1384
|
+
<div>
|
|
1385
|
+
<h4 style={{
|
|
1386
|
+
fontSize: 'var(--font-size-body-md)',
|
|
1387
|
+
fontWeight: 'var(--font-weight-bold)',
|
|
1388
|
+
color: 'var(--color-text-primary)',
|
|
1389
|
+
marginBottom: 'var(--gap-sm)',
|
|
1390
|
+
}}>
|
|
1391
|
+
🔍 Search Tips
|
|
1392
|
+
</h4>
|
|
1393
|
+
<div style={{
|
|
1394
|
+
padding: 'var(--space-component-sm)',
|
|
1395
|
+
background: 'var(--color-bg-surface)',
|
|
1396
|
+
borderRadius: 'var(--radius-md)',
|
|
1397
|
+
border: '1px solid var(--color-border-subtle)',
|
|
1398
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1399
|
+
color: 'var(--color-text-secondary)',
|
|
1400
|
+
}}>
|
|
1401
|
+
<p style={{ marginBottom: 'var(--gap-xs)' }}>
|
|
1402
|
+
The search box performs text matching across:
|
|
1403
|
+
</p>
|
|
1404
|
+
<ul style={{ marginLeft: '20px', marginBottom: 'var(--gap-xs)' }}>
|
|
1405
|
+
<li>Trace IDs</li>
|
|
1406
|
+
<li>Span names and operation names</li>
|
|
1407
|
+
<li>Span attributes (key-value pairs)</li>
|
|
1408
|
+
</ul>
|
|
1409
|
+
<p style={{ fontStyle: 'italic', color: 'var(--color-text-tertiary)' }}>
|
|
1410
|
+
For advanced queries, use the DuckDB SQL tab to write custom queries.
|
|
1411
|
+
</p>
|
|
1412
|
+
</div>
|
|
1413
|
+
</div>
|
|
1414
|
+
|
|
1415
|
+
<div>
|
|
1416
|
+
<h4 style={{
|
|
1417
|
+
fontSize: 'var(--font-size-body-md)',
|
|
1418
|
+
fontWeight: 'var(--font-weight-bold)',
|
|
1419
|
+
color: 'var(--color-text-primary)',
|
|
1420
|
+
marginBottom: 'var(--gap-sm)',
|
|
1421
|
+
}}>
|
|
1422
|
+
💡 DuckDB SQL Examples
|
|
1423
|
+
</h4>
|
|
1424
|
+
<div style={{
|
|
1425
|
+
display: 'grid',
|
|
1426
|
+
gap: 'var(--gap-sm)',
|
|
1427
|
+
}}>
|
|
1428
|
+
{[
|
|
1429
|
+
{ title: 'Find slow operations', query: 'SELECT service_name, name, duration_ms FROM spans WHERE duration_ms > 1000 ORDER BY duration_ms DESC LIMIT 10' },
|
|
1430
|
+
{ title: 'Error rate by service', query: 'SELECT service_name, COUNT(*) as total, SUM(CASE WHEN status_code = \'ERROR\' THEN 1 ELSE 0 END) as errors FROM spans GROUP BY service_name' },
|
|
1431
|
+
{ title: 'Recent traces', query: 'SELECT DISTINCT trace_id, MIN(timestamp) as start_time FROM spans GROUP BY trace_id ORDER BY start_time DESC LIMIT 20' },
|
|
1432
|
+
{ title: 'Operation hotspots', query: 'SELECT name, COUNT(*) as call_count, AVG(duration_ms) as avg_duration FROM spans GROUP BY name ORDER BY call_count DESC LIMIT 10' },
|
|
1433
|
+
].map((example) => (
|
|
1434
|
+
<div
|
|
1435
|
+
key={example.title}
|
|
1436
|
+
style={{
|
|
1437
|
+
padding: 'var(--space-component-sm)',
|
|
1438
|
+
background: 'var(--color-bg-surface)',
|
|
1439
|
+
borderRadius: 'var(--radius-md)',
|
|
1440
|
+
border: '1px solid var(--color-border-subtle)',
|
|
1441
|
+
}}
|
|
1442
|
+
>
|
|
1443
|
+
<div style={{
|
|
1444
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
1445
|
+
color: 'var(--color-text-primary)',
|
|
1446
|
+
marginBottom: '4px',
|
|
1447
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1448
|
+
}}>
|
|
1449
|
+
{example.title}
|
|
1450
|
+
</div>
|
|
1451
|
+
<code style={{
|
|
1452
|
+
display: 'block',
|
|
1453
|
+
padding: '8px',
|
|
1454
|
+
background: 'var(--color-bg-elevated)',
|
|
1455
|
+
borderRadius: 'var(--radius-sm)',
|
|
1456
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1457
|
+
fontFamily: 'var(--font-family-mono)',
|
|
1458
|
+
color: 'var(--color-text-secondary)',
|
|
1459
|
+
overflowX: 'auto',
|
|
1460
|
+
whiteSpace: 'pre-wrap',
|
|
1461
|
+
wordBreak: 'break-all',
|
|
1462
|
+
}}>
|
|
1463
|
+
{example.query}
|
|
1464
|
+
</code>
|
|
1465
|
+
</div>
|
|
1466
|
+
))}
|
|
1467
|
+
</div>
|
|
1468
|
+
</div>
|
|
1469
|
+
|
|
1470
|
+
<div>
|
|
1471
|
+
<h4 style={{
|
|
1472
|
+
fontSize: 'var(--font-size-body-md)',
|
|
1473
|
+
fontWeight: 'var(--font-weight-bold)',
|
|
1474
|
+
color: 'var(--color-text-primary)',
|
|
1475
|
+
marginBottom: 'var(--gap-sm)',
|
|
1476
|
+
}}>
|
|
1477
|
+
⚡ Best Practices
|
|
1478
|
+
</h4>
|
|
1479
|
+
<div style={{
|
|
1480
|
+
display: 'grid',
|
|
1481
|
+
gap: 'var(--gap-xs)',
|
|
1482
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1483
|
+
color: 'var(--color-text-secondary)',
|
|
1484
|
+
}}>
|
|
1485
|
+
{[
|
|
1486
|
+
'Use service filters to focus on specific components',
|
|
1487
|
+
'Blacklist noisy operations to reduce clutter',
|
|
1488
|
+
'Check RED metrics for quick health overview',
|
|
1489
|
+
'Use Dependencies view to understand service communication',
|
|
1490
|
+
'Write SQL queries for custom analysis and reporting',
|
|
1491
|
+
'Monitor error traces to identify failure patterns',
|
|
1492
|
+
].map((tip, idx) => (
|
|
1493
|
+
<div
|
|
1494
|
+
key={idx}
|
|
1495
|
+
style={{
|
|
1496
|
+
padding: '8px 12px',
|
|
1497
|
+
background: 'var(--color-bg-surface)',
|
|
1498
|
+
borderRadius: 'var(--radius-sm)',
|
|
1499
|
+
border: '1px solid var(--color-border-subtle)',
|
|
1500
|
+
}}
|
|
1501
|
+
>
|
|
1502
|
+
✓ {tip}
|
|
1503
|
+
</div>
|
|
1504
|
+
))}
|
|
1505
|
+
</div>
|
|
1506
|
+
</div>
|
|
1507
|
+
|
|
1508
|
+
<div style={{
|
|
1509
|
+
padding: 'var(--space-component-sm)',
|
|
1510
|
+
background: 'rgba(59, 130, 246, 0.1)',
|
|
1511
|
+
border: '1px solid #3b82f6',
|
|
1512
|
+
borderRadius: 'var(--radius-md)',
|
|
1513
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1514
|
+
color: 'var(--color-text-secondary)',
|
|
1515
|
+
}}>
|
|
1516
|
+
<div style={{ fontWeight: 'var(--font-weight-bold)', color: '#3b82f6', marginBottom: '4px' }}>
|
|
1517
|
+
📚 Full Documentation
|
|
1518
|
+
</div>
|
|
1519
|
+
For comprehensive tracing guides, see <code style={{ fontFamily: 'var(--font-family-mono)' }}>docs/how_to_use_tracing_effectively.md</code>
|
|
1520
|
+
</div>
|
|
1521
|
+
</div>
|
|
1522
|
+
)}
|
|
1523
|
+
|
|
1524
|
+
{/* SQL Query View */}
|
|
1525
|
+
{viewMode === 'sql' && (
|
|
1526
|
+
<div style={{
|
|
1527
|
+
display: 'flex',
|
|
1528
|
+
flexDirection: 'column',
|
|
1529
|
+
gap: 'var(--gap-sm)',
|
|
1530
|
+
height: '100%',
|
|
1531
|
+
}}>
|
|
1532
|
+
{/* Compact SQL Editor */}
|
|
1533
|
+
<div style={{
|
|
1534
|
+
display: 'flex',
|
|
1535
|
+
flexDirection: 'column',
|
|
1536
|
+
gap: 'var(--gap-xs)',
|
|
1537
|
+
flexShrink: 0,
|
|
1538
|
+
}}>
|
|
1539
|
+
<div style={{
|
|
1540
|
+
display: 'flex',
|
|
1541
|
+
justifyContent: 'space-between',
|
|
1542
|
+
alignItems: 'center',
|
|
1543
|
+
gap: 'var(--gap-sm)',
|
|
1544
|
+
}}>
|
|
1545
|
+
<div style={{
|
|
1546
|
+
display: 'flex',
|
|
1547
|
+
gap: 'var(--gap-xs)',
|
|
1548
|
+
flexWrap: 'wrap',
|
|
1549
|
+
flex: 1,
|
|
1550
|
+
alignItems: 'center',
|
|
1551
|
+
}}>
|
|
1552
|
+
<span style={{
|
|
1553
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1554
|
+
color: 'var(--color-text-tertiary)',
|
|
1555
|
+
flexShrink: 0,
|
|
1556
|
+
}}>
|
|
1557
|
+
Quick:
|
|
1558
|
+
</span>
|
|
1559
|
+
{[
|
|
1560
|
+
{ label: 'All', query: 'SELECT * FROM spans LIMIT 10' },
|
|
1561
|
+
{ label: 'By Service', query: 'SELECT service_name, COUNT(*) FROM spans GROUP BY service_name' },
|
|
1562
|
+
{ label: 'Errors', query: 'SELECT * FROM spans WHERE status_code = \'ERROR\'' },
|
|
1563
|
+
{ label: 'Avg Duration', query: 'SELECT service_name, AVG(duration_ms) FROM spans GROUP BY service_name' },
|
|
1564
|
+
].map((example) => (
|
|
1565
|
+
<button
|
|
1566
|
+
key={example.label}
|
|
1567
|
+
onClick={() => setSqlQuery(example.query)}
|
|
1568
|
+
style={{
|
|
1569
|
+
padding: '4px 8px',
|
|
1570
|
+
background: 'var(--color-bg-elevated)',
|
|
1571
|
+
color: 'var(--color-text-secondary)',
|
|
1572
|
+
border: '1px solid var(--color-border-subtle)',
|
|
1573
|
+
borderRadius: 'var(--radius-sm)',
|
|
1574
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1575
|
+
cursor: 'pointer',
|
|
1576
|
+
whiteSpace: 'nowrap',
|
|
1577
|
+
}}
|
|
1578
|
+
>
|
|
1579
|
+
{example.label}
|
|
1580
|
+
</button>
|
|
1581
|
+
))}
|
|
1582
|
+
</div>
|
|
1583
|
+
<button
|
|
1584
|
+
onClick={executeSqlQuery}
|
|
1585
|
+
disabled={sqlLoading}
|
|
1586
|
+
style={{
|
|
1587
|
+
padding: '6px 12px',
|
|
1588
|
+
background: sqlLoading ? 'var(--color-bg-surface)' : 'var(--color-accent)',
|
|
1589
|
+
color: sqlLoading ? 'var(--color-text-tertiary)' : 'white',
|
|
1590
|
+
border: 'none',
|
|
1591
|
+
borderRadius: 'var(--radius-md)',
|
|
1592
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1593
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
1594
|
+
cursor: sqlLoading ? 'not-allowed' : 'pointer',
|
|
1595
|
+
display: 'flex',
|
|
1596
|
+
alignItems: 'center',
|
|
1597
|
+
gap: '6px',
|
|
1598
|
+
flexShrink: 0,
|
|
1599
|
+
}}
|
|
1600
|
+
>
|
|
1601
|
+
{sqlLoading ? (
|
|
1602
|
+
<>
|
|
1603
|
+
<span>⏳</span>
|
|
1604
|
+
<span>Running...</span>
|
|
1605
|
+
</>
|
|
1606
|
+
) : (
|
|
1607
|
+
<>
|
|
1608
|
+
<span>▶️</span>
|
|
1609
|
+
<span>Run (Cmd+Enter)</span>
|
|
1610
|
+
</>
|
|
1611
|
+
)}
|
|
1612
|
+
</button>
|
|
1613
|
+
</div>
|
|
1614
|
+
|
|
1615
|
+
<textarea
|
|
1616
|
+
value={sqlQuery}
|
|
1617
|
+
onChange={(e) => setSqlQuery(e.target.value)}
|
|
1618
|
+
onKeyDown={(e) => {
|
|
1619
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
1620
|
+
e.preventDefault();
|
|
1621
|
+
executeSqlQuery();
|
|
1622
|
+
}
|
|
1623
|
+
}}
|
|
1624
|
+
placeholder="SELECT * FROM spans WHERE service_name = 'my-service' LIMIT 100"
|
|
1625
|
+
style={{
|
|
1626
|
+
width: '100%',
|
|
1627
|
+
minHeight: '60px',
|
|
1628
|
+
maxHeight: '120px',
|
|
1629
|
+
padding: '8px 12px',
|
|
1630
|
+
fontFamily: 'var(--font-family-mono)',
|
|
1631
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1632
|
+
background: 'var(--color-bg-surface)',
|
|
1633
|
+
color: 'var(--color-text-primary)',
|
|
1634
|
+
border: '1px solid var(--color-border-subtle)',
|
|
1635
|
+
borderRadius: 'var(--radius-md)',
|
|
1636
|
+
resize: 'vertical',
|
|
1637
|
+
lineHeight: '1.5',
|
|
1638
|
+
}}
|
|
1639
|
+
/>
|
|
1640
|
+
</div>
|
|
1641
|
+
|
|
1642
|
+
{/* Error Display */}
|
|
1643
|
+
{sqlError && (
|
|
1644
|
+
<div style={{
|
|
1645
|
+
padding: '8px 12px',
|
|
1646
|
+
background: 'rgba(239, 68, 68, 0.1)',
|
|
1647
|
+
border: '1px solid #ef4444',
|
|
1648
|
+
borderRadius: 'var(--radius-md)',
|
|
1649
|
+
color: '#ef4444',
|
|
1650
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1651
|
+
fontFamily: 'var(--font-family-mono)',
|
|
1652
|
+
display: 'flex',
|
|
1653
|
+
gap: '8px',
|
|
1654
|
+
alignItems: 'center',
|
|
1655
|
+
flexShrink: 0,
|
|
1656
|
+
}}>
|
|
1657
|
+
<span>❌</span>
|
|
1658
|
+
<span>{sqlError}</span>
|
|
1659
|
+
</div>
|
|
1660
|
+
)}
|
|
1661
|
+
|
|
1662
|
+
{/* Results Table */}
|
|
1663
|
+
{sqlResults && sqlResults.length > 0 && (
|
|
1664
|
+
<div style={{
|
|
1665
|
+
flex: 1,
|
|
1666
|
+
display: 'flex',
|
|
1667
|
+
flexDirection: 'column',
|
|
1668
|
+
gap: 'var(--gap-xs)',
|
|
1669
|
+
minHeight: 0,
|
|
1670
|
+
}}>
|
|
1671
|
+
<div style={{
|
|
1672
|
+
display: 'flex',
|
|
1673
|
+
justifyContent: 'space-between',
|
|
1674
|
+
alignItems: 'center',
|
|
1675
|
+
}}>
|
|
1676
|
+
<div style={{
|
|
1677
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
1678
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
1679
|
+
color: 'var(--color-text-secondary)',
|
|
1680
|
+
}}>
|
|
1681
|
+
Results ({sqlResults.length} row{sqlResults.length !== 1 ? 's' : ''}, {sqlColumns.length} column{sqlColumns.length !== 1 ? 's' : ''})
|
|
1682
|
+
</div>
|
|
1683
|
+
<button
|
|
1684
|
+
onClick={() => {
|
|
1685
|
+
// Generate CSV
|
|
1686
|
+
const csv = [
|
|
1687
|
+
sqlColumns.join(','), // Header
|
|
1688
|
+
...sqlResults.map(row =>
|
|
1689
|
+
sqlColumns.map(col => {
|
|
1690
|
+
const val = row[col];
|
|
1691
|
+
if (val === null || val === undefined) return '';
|
|
1692
|
+
const str = String(val);
|
|
1693
|
+
// Escape quotes and wrap in quotes if contains comma, quote, or newline
|
|
1694
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
1695
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
1696
|
+
}
|
|
1697
|
+
return str;
|
|
1698
|
+
}).join(',')
|
|
1699
|
+
)
|
|
1700
|
+
].join('\n');
|
|
1701
|
+
|
|
1702
|
+
// Download CSV
|
|
1703
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
1704
|
+
const link = document.createElement('a');
|
|
1705
|
+
const url = URL.createObjectURL(blob);
|
|
1706
|
+
link.setAttribute('href', url);
|
|
1707
|
+
link.setAttribute('download', `trace-query-${new Date().toISOString().split('T')[0]}.csv`);
|
|
1708
|
+
link.style.visibility = 'hidden';
|
|
1709
|
+
document.body.appendChild(link);
|
|
1710
|
+
link.click();
|
|
1711
|
+
document.body.removeChild(link);
|
|
1712
|
+
}}
|
|
1713
|
+
style={{
|
|
1714
|
+
padding: '4px 10px',
|
|
1715
|
+
background: 'var(--color-bg-elevated)',
|
|
1716
|
+
color: 'var(--color-text-secondary)',
|
|
1717
|
+
border: '1px solid var(--color-border-subtle)',
|
|
1718
|
+
borderRadius: 'var(--radius-sm)',
|
|
1719
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1720
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
1721
|
+
cursor: 'pointer',
|
|
1722
|
+
display: 'flex',
|
|
1723
|
+
alignItems: 'center',
|
|
1724
|
+
gap: '4px',
|
|
1725
|
+
}}
|
|
1726
|
+
>
|
|
1727
|
+
<span>📥</span>
|
|
1728
|
+
<span>Export CSV</span>
|
|
1729
|
+
</button>
|
|
1730
|
+
</div>
|
|
1731
|
+
|
|
1732
|
+
<div style={{
|
|
1733
|
+
flex: 1,
|
|
1734
|
+
overflow: 'auto',
|
|
1735
|
+
background: 'var(--color-bg-surface)',
|
|
1736
|
+
border: '1px solid var(--color-border-subtle)',
|
|
1737
|
+
borderRadius: 'var(--radius-md)',
|
|
1738
|
+
}}>
|
|
1739
|
+
<table style={{
|
|
1740
|
+
width: '100%',
|
|
1741
|
+
borderCollapse: 'collapse',
|
|
1742
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1743
|
+
}}>
|
|
1744
|
+
<thead style={{
|
|
1745
|
+
position: 'sticky',
|
|
1746
|
+
top: 0,
|
|
1747
|
+
background: 'var(--color-bg-elevated)',
|
|
1748
|
+
borderBottom: '2px solid var(--color-border-subtle)',
|
|
1749
|
+
zIndex: 1,
|
|
1750
|
+
}}>
|
|
1751
|
+
<tr>
|
|
1752
|
+
{sqlColumns.map((col) => (
|
|
1753
|
+
<th
|
|
1754
|
+
key={col}
|
|
1755
|
+
style={{
|
|
1756
|
+
padding: '10px 12px',
|
|
1757
|
+
textAlign: 'left',
|
|
1758
|
+
fontWeight: 'var(--font-weight-bold)',
|
|
1759
|
+
color: 'var(--color-text-secondary)',
|
|
1760
|
+
whiteSpace: 'nowrap',
|
|
1761
|
+
borderRight: '1px solid var(--color-border-subtle)',
|
|
1762
|
+
}}
|
|
1763
|
+
>
|
|
1764
|
+
{col}
|
|
1765
|
+
</th>
|
|
1766
|
+
))}
|
|
1767
|
+
</tr>
|
|
1768
|
+
</thead>
|
|
1769
|
+
<tbody>
|
|
1770
|
+
{sqlResults.map((row, idx) => (
|
|
1771
|
+
<tr
|
|
1772
|
+
key={idx}
|
|
1773
|
+
style={{
|
|
1774
|
+
borderBottom: '1px solid var(--color-border-subtle)',
|
|
1775
|
+
background: idx % 2 === 0 ? 'transparent' : 'rgba(255, 255, 255, 0.02)',
|
|
1776
|
+
}}
|
|
1777
|
+
>
|
|
1778
|
+
{sqlColumns.map((col) => (
|
|
1779
|
+
<td
|
|
1780
|
+
key={col}
|
|
1781
|
+
style={{
|
|
1782
|
+
padding: '8px 12px',
|
|
1783
|
+
color: 'var(--color-text-primary)',
|
|
1784
|
+
fontFamily: typeof row[col] === 'string' && row[col].length > 50 ? 'var(--font-family-mono)' : 'inherit',
|
|
1785
|
+
maxWidth: '400px',
|
|
1786
|
+
overflow: 'hidden',
|
|
1787
|
+
textOverflow: 'ellipsis',
|
|
1788
|
+
whiteSpace: 'nowrap',
|
|
1789
|
+
borderRight: '1px solid var(--color-border-subtle)',
|
|
1790
|
+
}}
|
|
1791
|
+
title={String(row[col])}
|
|
1792
|
+
>
|
|
1793
|
+
{row[col] === null || row[col] === undefined ? (
|
|
1794
|
+
<span style={{ color: 'var(--color-text-tertiary)', fontStyle: 'italic' }}>null</span>
|
|
1795
|
+
) : (
|
|
1796
|
+
String(row[col])
|
|
1797
|
+
)}
|
|
1798
|
+
</td>
|
|
1799
|
+
))}
|
|
1800
|
+
</tr>
|
|
1801
|
+
))}
|
|
1802
|
+
</tbody>
|
|
1803
|
+
</table>
|
|
1804
|
+
</div>
|
|
1805
|
+
</div>
|
|
1806
|
+
)}
|
|
1807
|
+
|
|
1808
|
+
{/* Empty State */}
|
|
1809
|
+
{!sqlLoading && !sqlError && (!sqlResults || sqlResults.length === 0) && (
|
|
1810
|
+
<div style={{
|
|
1811
|
+
textAlign: 'center',
|
|
1812
|
+
padding: 'var(--space-component-xl)',
|
|
1813
|
+
color: 'var(--color-text-secondary)',
|
|
1814
|
+
}}>
|
|
1815
|
+
<div style={{ fontSize: '32px', marginBottom: 'var(--gap-md)', opacity: 0.5 }}>📊</div>
|
|
1816
|
+
<div>Run a query to see results</div>
|
|
1817
|
+
<div style={{ fontSize: 'var(--font-size-body-xs)', marginTop: 'var(--gap-xs)', opacity: 0.7 }}>
|
|
1818
|
+
Try one of the example queries above
|
|
1819
|
+
</div>
|
|
1820
|
+
</div>
|
|
1821
|
+
)}
|
|
1822
|
+
</div>
|
|
1823
|
+
)}
|
|
1824
|
+
|
|
1825
|
+
{/* Traces View (Timeline & Statistics) */}
|
|
1826
|
+
{(viewMode === 'timeline' || viewMode === 'statistics') && (
|
|
1827
|
+
<>
|
|
1828
|
+
{filteredTraces.length === 0 ? (
|
|
1829
|
+
<div style={{
|
|
1830
|
+
textAlign: 'center',
|
|
1831
|
+
padding: 'var(--space-component-xl)',
|
|
1832
|
+
color: 'var(--color-text-secondary)',
|
|
1833
|
+
}}>
|
|
1834
|
+
<div style={{ fontSize: '32px', marginBottom: 'var(--gap-md)', opacity: 0.5 }}>🔎</div>
|
|
1835
|
+
<div>No traces found</div>
|
|
1836
|
+
</div>
|
|
1837
|
+
) : (
|
|
1838
|
+
filteredTraces.map((trace) => (
|
|
1839
|
+
<div
|
|
1840
|
+
key={trace.traceId}
|
|
1841
|
+
style={{
|
|
1842
|
+
marginBottom: 'var(--space-component-lg)',
|
|
1843
|
+
background: 'var(--color-bg-elevated)',
|
|
1844
|
+
borderRadius: 'var(--radius-lg)',
|
|
1845
|
+
border: `1px solid ${trace.hasError ? '#ef4444' : 'var(--color-border-subtle)'}`,
|
|
1846
|
+
overflow: 'hidden',
|
|
1847
|
+
}}
|
|
1848
|
+
>
|
|
1849
|
+
<div
|
|
1850
|
+
onClick={() => {
|
|
1851
|
+
setSelectedTraceIds(prev => {
|
|
1852
|
+
const newSet = new Set(prev);
|
|
1853
|
+
if (newSet.has(trace.traceId)) {
|
|
1854
|
+
newSet.delete(trace.traceId);
|
|
1855
|
+
} else {
|
|
1856
|
+
newSet.add(trace.traceId);
|
|
1857
|
+
}
|
|
1858
|
+
return newSet;
|
|
1859
|
+
});
|
|
1860
|
+
}}
|
|
1861
|
+
style={{
|
|
1862
|
+
padding: 'var(--space-component-md)',
|
|
1863
|
+
background: trace.hasError ? 'rgba(239, 68, 68, 0.1)' : 'var(--color-bg-surface)',
|
|
1864
|
+
cursor: 'pointer',
|
|
1865
|
+
display: 'flex',
|
|
1866
|
+
flexDirection: 'column',
|
|
1867
|
+
gap: 'var(--gap-sm)',
|
|
1868
|
+
}}
|
|
1869
|
+
>
|
|
1870
|
+
{/* Top row: Status, Duration, Services */}
|
|
1871
|
+
<div style={{
|
|
1872
|
+
display: 'flex',
|
|
1873
|
+
alignItems: 'center',
|
|
1874
|
+
gap: 'var(--gap-md)',
|
|
1875
|
+
}}>
|
|
1876
|
+
<div style={{ fontSize: '14px', opacity: 0.5 }}>
|
|
1877
|
+
{selectedTraceIds.has(trace.traceId) ? '▼' : '►'}
|
|
1878
|
+
</div>
|
|
1879
|
+
|
|
1880
|
+
{/* Status indicator */}
|
|
1881
|
+
<div style={{
|
|
1882
|
+
display: 'flex',
|
|
1883
|
+
alignItems: 'center',
|
|
1884
|
+
gap: '6px',
|
|
1885
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
1886
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
1887
|
+
color: trace.hasError ? '#ef4444' : '#10b981',
|
|
1888
|
+
}}>
|
|
1889
|
+
<span>{trace.hasError ? '✗' : '✓'}</span>
|
|
1890
|
+
<span>{trace.hasError ? 'ERROR' : 'OK'}</span>
|
|
1891
|
+
</div>
|
|
1892
|
+
|
|
1893
|
+
{/* Duration */}
|
|
1894
|
+
<div style={{
|
|
1895
|
+
fontSize: 'var(--font-size-body-sm)',
|
|
1896
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
1897
|
+
color: 'var(--color-text-primary)',
|
|
1898
|
+
}}>
|
|
1899
|
+
{trace.duration.toFixed(2)}ms
|
|
1900
|
+
</div>
|
|
1901
|
+
|
|
1902
|
+
{/* Service badges */}
|
|
1903
|
+
<div style={{
|
|
1904
|
+
display: 'flex',
|
|
1905
|
+
gap: '6px',
|
|
1906
|
+
flex: 1,
|
|
1907
|
+
flexWrap: 'wrap',
|
|
1908
|
+
}}>
|
|
1909
|
+
{Array.from(trace.services).map(service => {
|
|
1910
|
+
const serviceColor = serviceColors.colorMap.get(service) || '#6366f1';
|
|
1911
|
+
return (
|
|
1912
|
+
<div
|
|
1913
|
+
key={service}
|
|
1914
|
+
style={{
|
|
1915
|
+
padding: '4px 12px',
|
|
1916
|
+
borderRadius: 'var(--radius-sm)',
|
|
1917
|
+
background: serviceColor,
|
|
1918
|
+
color: 'white',
|
|
1919
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1920
|
+
fontWeight: 'var(--font-weight-medium)',
|
|
1921
|
+
whiteSpace: 'nowrap',
|
|
1922
|
+
}}
|
|
1923
|
+
>
|
|
1924
|
+
{service}
|
|
1925
|
+
</div>
|
|
1926
|
+
);
|
|
1927
|
+
})}
|
|
1928
|
+
</div>
|
|
1929
|
+
</div>
|
|
1930
|
+
|
|
1931
|
+
{/* Bottom row: Trace ID, Span count, Timestamp */}
|
|
1932
|
+
<div style={{
|
|
1933
|
+
display: 'flex',
|
|
1934
|
+
alignItems: 'center',
|
|
1935
|
+
gap: 'var(--gap-md)',
|
|
1936
|
+
paddingLeft: '30px',
|
|
1937
|
+
fontSize: 'var(--font-size-body-xs)',
|
|
1938
|
+
color: 'var(--color-text-tertiary)',
|
|
1939
|
+
}}>
|
|
1940
|
+
<div style={{
|
|
1941
|
+
fontFamily: 'var(--font-family-mono)',
|
|
1942
|
+
color: 'var(--color-text-secondary)',
|
|
1943
|
+
}}>
|
|
1944
|
+
{trace.traceId.slice(0, 16)}...
|
|
1945
|
+
</div>
|
|
1946
|
+
<span>•</span>
|
|
1947
|
+
<span>{trace.spanCount} spans</span>
|
|
1948
|
+
<span>•</span>
|
|
1949
|
+
<span>{trace.services.size} service{trace.services.size !== 1 ? 's' : ''}</span>
|
|
1950
|
+
<span>•</span>
|
|
1951
|
+
<span>{new Date(trace.startTime / 1_000_000).toLocaleTimeString()}</span>
|
|
1952
|
+
</div>
|
|
1953
|
+
</div>
|
|
1954
|
+
|
|
1955
|
+
{selectedTraceIds.has(trace.traceId) && (
|
|
1956
|
+
<div style={{ padding: 'var(--space-component-md)', paddingTop: 0 }}>
|
|
1957
|
+
{viewMode === 'timeline' && renderTimelineView(trace)}
|
|
1958
|
+
{viewMode === 'statistics' && renderStatisticsView(trace)}
|
|
1959
|
+
</div>
|
|
1960
|
+
)}
|
|
1961
|
+
</div>
|
|
1962
|
+
))
|
|
1963
|
+
)}
|
|
1964
|
+
</>
|
|
1965
|
+
)}
|
|
1966
|
+
</div>
|
|
1967
|
+
</div>
|
|
1968
|
+
);
|
|
1969
|
+
};
|
|
1970
|
+
|
|
1971
|
+
export default TraceModuleJaeger;
|