autotel-terminal 17.0.1 → 17.0.3
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/dist/cli.cjs +1314 -262
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1314 -262
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1264 -241
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1264 -241
- package/dist/index.js.map +1 -1
- package/package.json +14 -7
- package/src/ai/provider.test.ts +47 -0
- package/src/ai/provider.ts +124 -0
- package/src/ai/stream.ts +44 -0
- package/src/ai/system-prompt.ts +24 -0
- package/src/ai/tools.ts +233 -0
- package/src/ai/types.ts +19 -0
- package/src/cli-stream.ts +0 -1
- package/src/cli.integration.test.ts +11 -16
- package/src/cli.ts +51 -19
- package/src/index.tsx +1446 -466
- package/src/lib/dashboard-keymap.test.ts +33 -17
- package/src/lib/dashboard-keymap.ts +1 -7
- package/src/lib/error-model.test.ts +0 -1
- package/src/lib/error-model.ts +3 -2
- package/src/lib/export-model.test.ts +0 -1
- package/src/lib/export-model.ts +0 -1
- package/src/lib/filters.test.ts +28 -2
- package/src/lib/filters.ts +5 -2
- package/src/lib/format.test.ts +33 -1
- package/src/lib/format.ts +28 -0
- package/src/lib/log-model.ts +3 -5
- package/src/lib/recording-regression.test.ts +6 -2
- package/src/lib/stats-model.test.ts +13 -4
- package/src/lib/stats-model.ts +3 -5
- package/src/lib/topology-model.test.ts +6 -5
- package/src/lib/topology-model.ts +1 -5
- package/src/lib/trace-model.test.ts +84 -13
- package/src/lib/trace-model.ts +18 -11
- package/src/log-stream.ts +0 -1
- package/src/otlp-http-json.test.ts +11 -5
- package/src/otlp-http-json.ts +39 -12
package/dist/cli.cjs
CHANGED
|
@@ -8,6 +8,8 @@ var react = require('react');
|
|
|
8
8
|
var ink = require('ink');
|
|
9
9
|
var autotel = require('autotel');
|
|
10
10
|
var tracerProvider = require('autotel/tracer-provider');
|
|
11
|
+
var ai = require('ai');
|
|
12
|
+
var zod = require('zod');
|
|
11
13
|
require('autotel/exporters');
|
|
12
14
|
var jsxRuntime = require('react/jsx-runtime');
|
|
13
15
|
|
|
@@ -200,7 +202,8 @@ function sortSpansForWaterfall(spans) {
|
|
|
200
202
|
}
|
|
201
203
|
const withDepth = spans.map((s) => ({ span: s, depth: depth(s) }));
|
|
202
204
|
withDepth.sort((a, b) => {
|
|
203
|
-
if (a.span.startTime !== b.span.startTime)
|
|
205
|
+
if (a.span.startTime !== b.span.startTime)
|
|
206
|
+
return a.span.startTime - b.span.startTime;
|
|
204
207
|
return a.depth - b.depth;
|
|
205
208
|
});
|
|
206
209
|
return withDepth;
|
|
@@ -215,7 +218,9 @@ function filterTraceSummaries(summaries, searchQuery, errorsOnly) {
|
|
|
215
218
|
let list = summaries;
|
|
216
219
|
if (searchQuery.trim() === "") return list;
|
|
217
220
|
const q = searchQuery.toLowerCase();
|
|
218
|
-
return list.filter(
|
|
221
|
+
return list.filter(
|
|
222
|
+
(s) => s.spans.some((sp) => sp.name.toLowerCase().includes(q))
|
|
223
|
+
);
|
|
219
224
|
}
|
|
220
225
|
function computeStats(spans) {
|
|
221
226
|
const total = spans.length;
|
|
@@ -260,6 +265,17 @@ function truncate(s, width) {
|
|
|
260
265
|
if (s.length <= width) return s;
|
|
261
266
|
return s.slice(0, Math.max(0, width - 1)) + "\u2026";
|
|
262
267
|
}
|
|
268
|
+
function buildWaterfallBar(spanStart, spanDuration, traceStart, traceDuration, width) {
|
|
269
|
+
if (traceDuration <= 0) return " ".repeat(width);
|
|
270
|
+
const offsetRatio = (spanStart - traceStart) / traceDuration;
|
|
271
|
+
const widthRatio = spanDuration / traceDuration;
|
|
272
|
+
const barStart = Math.max(0, Math.floor(offsetRatio * width));
|
|
273
|
+
const barLen = Math.max(1, Math.round(widthRatio * width));
|
|
274
|
+
const clampedStart = Math.min(barStart, width - 1);
|
|
275
|
+
const clampedLen = Math.min(barLen, width - clampedStart);
|
|
276
|
+
const trailing = Math.max(0, width - clampedStart - clampedLen);
|
|
277
|
+
return " ".repeat(clampedStart) + "\u2588".repeat(clampedLen) + " ".repeat(trailing);
|
|
278
|
+
}
|
|
263
279
|
|
|
264
280
|
// src/lib/log-model.ts
|
|
265
281
|
function filterLogsBySearch(logs, searchQuery, minLevel) {
|
|
@@ -423,10 +439,11 @@ function getStatusCode(span) {
|
|
|
423
439
|
return void 0;
|
|
424
440
|
}
|
|
425
441
|
function applySpanFilters(spans, state) {
|
|
426
|
-
const { serviceName, route, statusGroup, errorsOnly, searchQuery } = state;
|
|
442
|
+
const { serviceName, route, statusGroup, errorsOnly, searchQuery, traceId } = state;
|
|
427
443
|
const q = searchQuery?.trim().toLowerCase() ?? "";
|
|
428
444
|
return spans.filter((span) => {
|
|
429
445
|
const attrs = span.attributes ?? {};
|
|
446
|
+
if (traceId && span.traceId !== traceId) return false;
|
|
430
447
|
if (errorsOnly && span.status !== "ERROR") return false;
|
|
431
448
|
if (serviceName) {
|
|
432
449
|
const svc = getServiceName(span);
|
|
@@ -482,17 +499,17 @@ function getStatusCodeFromSpans(spans) {
|
|
|
482
499
|
}
|
|
483
500
|
function buildErrorSummaries(traceSummaries) {
|
|
484
501
|
const out = [];
|
|
485
|
-
for (const
|
|
486
|
-
const errorSpans =
|
|
502
|
+
for (const t2 of traceSummaries) {
|
|
503
|
+
const errorSpans = t2.spans.filter((s) => s.status === "ERROR");
|
|
487
504
|
if (errorSpans.length === 0) continue;
|
|
488
505
|
out.push({
|
|
489
|
-
traceId:
|
|
490
|
-
rootName:
|
|
491
|
-
serviceName: getServiceNameFromSpans(
|
|
492
|
-
route: getRouteFromSpans(
|
|
493
|
-
statusCode: getStatusCodeFromSpans(
|
|
506
|
+
traceId: t2.traceId,
|
|
507
|
+
rootName: t2.rootName,
|
|
508
|
+
serviceName: getServiceNameFromSpans(t2.spans),
|
|
509
|
+
route: getRouteFromSpans(t2.spans),
|
|
510
|
+
statusCode: getStatusCodeFromSpans(t2.spans),
|
|
494
511
|
errorCount: errorSpans.length,
|
|
495
|
-
lastEndTime:
|
|
512
|
+
lastEndTime: t2.lastEndTime
|
|
496
513
|
});
|
|
497
514
|
}
|
|
498
515
|
out.sort((a, b) => b.lastEndTime - a.lastEndTime);
|
|
@@ -528,6 +545,302 @@ function exportTraceToJson(trace, logs) {
|
|
|
528
545
|
};
|
|
529
546
|
return JSON.stringify(exported, null, 2);
|
|
530
547
|
}
|
|
548
|
+
|
|
549
|
+
// src/ai/provider.ts
|
|
550
|
+
async function detectOllama(baseUrl = "http://127.0.0.1:11434") {
|
|
551
|
+
try {
|
|
552
|
+
const res = await fetch(`${baseUrl}/api/tags`, {
|
|
553
|
+
signal: AbortSignal.timeout(1e3)
|
|
554
|
+
});
|
|
555
|
+
return res.ok;
|
|
556
|
+
} catch {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function resolveConfig(options = {}) {
|
|
561
|
+
const provider = options.provider ?? process.env.AI_PROVIDER ?? void 0;
|
|
562
|
+
const model = options.model ?? process.env.AI_MODEL;
|
|
563
|
+
const apiKey = options.apiKey ?? process.env.AI_API_KEY ?? (provider === "openai" ? process.env.OPENAI_API_KEY : void 0);
|
|
564
|
+
const baseUrl = options.baseUrl ?? process.env.AI_BASE_URL;
|
|
565
|
+
if (provider) {
|
|
566
|
+
return {
|
|
567
|
+
provider,
|
|
568
|
+
model: model ?? (provider === "ollama" ? "granite4" : "gpt-4o"),
|
|
569
|
+
apiKey,
|
|
570
|
+
baseUrl
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
var defaultAutoDetectDeps = { detectOllama };
|
|
576
|
+
async function resolveConfigWithAutoDetect(options = {}, deps = defaultAutoDetectDeps) {
|
|
577
|
+
const config = resolveConfig(options);
|
|
578
|
+
if (config) return config;
|
|
579
|
+
const ollamaUrl = options.baseUrl ?? process.env.AI_BASE_URL ?? "http://127.0.0.1:11434";
|
|
580
|
+
if (await deps.detectOllama(ollamaUrl)) {
|
|
581
|
+
return {
|
|
582
|
+
provider: "ollama",
|
|
583
|
+
model: options.model ?? process.env.AI_MODEL ?? "granite4",
|
|
584
|
+
baseUrl: ollamaUrl
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
const openaiKey = options.apiKey ?? process.env.AI_API_KEY ?? process.env.OPENAI_API_KEY;
|
|
588
|
+
if (openaiKey) {
|
|
589
|
+
return {
|
|
590
|
+
provider: "openai",
|
|
591
|
+
model: options.model ?? process.env.AI_MODEL ?? "gpt-4o",
|
|
592
|
+
apiKey: openaiKey
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
async function createAIModel(config) {
|
|
598
|
+
switch (config.provider) {
|
|
599
|
+
case "ollama": {
|
|
600
|
+
const { createOllama } = await import('ai-sdk-ollama');
|
|
601
|
+
const ollama = createOllama({
|
|
602
|
+
baseURL: config.baseUrl ?? "http://127.0.0.1:11434"
|
|
603
|
+
});
|
|
604
|
+
return { model: ollama(config.model), providerType: "ollama", config };
|
|
605
|
+
}
|
|
606
|
+
case "openai": {
|
|
607
|
+
const { createOpenAI } = await import('@ai-sdk/openai');
|
|
608
|
+
const openai = createOpenAI({
|
|
609
|
+
apiKey: config.apiKey,
|
|
610
|
+
...config.baseUrl ? { baseURL: config.baseUrl } : {}
|
|
611
|
+
});
|
|
612
|
+
return { model: openai(config.model), providerType: "openai", config };
|
|
613
|
+
}
|
|
614
|
+
case "openai-compatible": {
|
|
615
|
+
const { createOpenAICompatible } = await import('@ai-sdk/openai-compatible');
|
|
616
|
+
const provider = createOpenAICompatible({
|
|
617
|
+
baseURL: config.baseUrl ?? "http://127.0.0.1:11434/v1",
|
|
618
|
+
name: "custom",
|
|
619
|
+
...config.apiKey ? { headers: { Authorization: `Bearer ${config.apiKey}` } } : {}
|
|
620
|
+
});
|
|
621
|
+
return {
|
|
622
|
+
model: provider(config.model),
|
|
623
|
+
providerType: "openai-compatible",
|
|
624
|
+
config
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
default: {
|
|
628
|
+
throw new Error(
|
|
629
|
+
`Unsupported provider: "${config.provider}". Expected "ollama", "openai", or "openai-compatible".`
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/ai/system-prompt.ts
|
|
636
|
+
function buildSystemPrompt(viewMode, contextJson) {
|
|
637
|
+
return `You are an OpenTelemetry expert assistant analyzing live telemetry data from a running application.
|
|
638
|
+
The user is viewing their ${viewMode} dashboard in a terminal TUI.
|
|
639
|
+
|
|
640
|
+
You have tools to query the telemetry data precisely. Use them to answer questions:
|
|
641
|
+
- getOverviewStats: high-level stats (spans, errors, latency)
|
|
642
|
+
- listServices: all services with error rates and p95
|
|
643
|
+
- findSlowestSpans: find slow spans, optionally by service
|
|
644
|
+
- findErrorTraces: find traces with errors
|
|
645
|
+
- getTraceDetail: deep dive into a specific trace
|
|
646
|
+
- searchSpans: search spans by name
|
|
647
|
+
- searchLogs: search logs by message content
|
|
648
|
+
|
|
649
|
+
Use tools first to gather data, then synthesize a concise answer.
|
|
650
|
+
Keep responses under 300 words.
|
|
651
|
+
Use specific span names, durations, and attribute values from the data.
|
|
652
|
+
Format for a narrow terminal column \u2014 use short paragraphs, not wide tables.
|
|
653
|
+
|
|
654
|
+
Current dashboard summary:
|
|
655
|
+
${contextJson}`;
|
|
656
|
+
}
|
|
657
|
+
var t = ai.tool;
|
|
658
|
+
function createTelemetryTools(ctx) {
|
|
659
|
+
return {
|
|
660
|
+
getOverviewStats: t({
|
|
661
|
+
description: "Get high-level stats: total spans, error count, average duration, p95 duration, and service count.",
|
|
662
|
+
parameters: zod.z.object({}),
|
|
663
|
+
execute: async () => ({
|
|
664
|
+
totalSpans: ctx.stats.total,
|
|
665
|
+
errors: ctx.stats.errors,
|
|
666
|
+
avgMs: Math.round(ctx.stats.avg),
|
|
667
|
+
p95Ms: Math.round(ctx.stats.p95),
|
|
668
|
+
serviceCount: ctx.serviceStats.length,
|
|
669
|
+
traceCount: ctx.traces.length
|
|
670
|
+
})
|
|
671
|
+
}),
|
|
672
|
+
listServices: t({
|
|
673
|
+
description: "List all services with their span counts, error counts, and p95 latencies.",
|
|
674
|
+
parameters: zod.z.object({}),
|
|
675
|
+
execute: async () => ctx.serviceStats.map((s) => ({
|
|
676
|
+
serviceName: s.serviceName,
|
|
677
|
+
total: s.total,
|
|
678
|
+
errors: s.errors,
|
|
679
|
+
avgMs: Math.round(s.avgMs),
|
|
680
|
+
p95Ms: Math.round(s.p95Ms)
|
|
681
|
+
}))
|
|
682
|
+
}),
|
|
683
|
+
findSlowestSpans: t({
|
|
684
|
+
description: "Find the slowest spans, optionally filtered by service name. Returns span name, duration, status, and key attributes.",
|
|
685
|
+
parameters: zod.z.object({
|
|
686
|
+
service: zod.z.string().optional().describe("Filter by service name"),
|
|
687
|
+
limit: zod.z.number().optional().describe("Max results (default 10)")
|
|
688
|
+
}),
|
|
689
|
+
execute: async ({
|
|
690
|
+
service,
|
|
691
|
+
limit
|
|
692
|
+
}) => {
|
|
693
|
+
const max = limit ?? 10;
|
|
694
|
+
let filtered = ctx.spans;
|
|
695
|
+
if (service) {
|
|
696
|
+
filtered = filtered.filter(
|
|
697
|
+
(s) => s.attributes?.["service.name"] === service
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
return filtered.toSorted((a, b) => b.durationMs - a.durationMs).slice(0, max).map((s) => ({
|
|
701
|
+
name: s.name,
|
|
702
|
+
durationMs: Math.round(s.durationMs),
|
|
703
|
+
status: s.status,
|
|
704
|
+
service: s.attributes?.["service.name"] ?? "unknown",
|
|
705
|
+
route: s.attributes?.["http.route"],
|
|
706
|
+
traceId: s.traceId.slice(0, 8)
|
|
707
|
+
}));
|
|
708
|
+
}
|
|
709
|
+
}),
|
|
710
|
+
findErrorTraces: t({
|
|
711
|
+
description: "Find traces that contain errors, with root span name, service, route, and error count.",
|
|
712
|
+
parameters: zod.z.object({
|
|
713
|
+
service: zod.z.string().optional().describe("Filter by service name"),
|
|
714
|
+
limit: zod.z.number().optional().describe("Max results (default 10)")
|
|
715
|
+
}),
|
|
716
|
+
execute: async ({
|
|
717
|
+
service,
|
|
718
|
+
limit
|
|
719
|
+
}) => {
|
|
720
|
+
const max = limit ?? 10;
|
|
721
|
+
let errors = ctx.errorSummaries;
|
|
722
|
+
if (service) {
|
|
723
|
+
errors = errors.filter((e) => e.serviceName === service);
|
|
724
|
+
}
|
|
725
|
+
return errors.slice(0, max).map((e) => ({
|
|
726
|
+
traceId: e.traceId.slice(0, 8),
|
|
727
|
+
rootName: e.rootName,
|
|
728
|
+
serviceName: e.serviceName,
|
|
729
|
+
route: e.route,
|
|
730
|
+
errorCount: e.errorCount
|
|
731
|
+
}));
|
|
732
|
+
}
|
|
733
|
+
}),
|
|
734
|
+
getTraceDetail: t({
|
|
735
|
+
description: "Get full detail of a specific trace by trace ID prefix. Returns all spans with their parent relationships, durations, and attributes.",
|
|
736
|
+
parameters: zod.z.object({
|
|
737
|
+
traceIdPrefix: zod.z.string().describe("First 8+ characters of the trace ID")
|
|
738
|
+
}),
|
|
739
|
+
execute: async ({ traceIdPrefix }) => {
|
|
740
|
+
const trace = ctx.traces.find(
|
|
741
|
+
(t2) => t2.traceId.startsWith(traceIdPrefix)
|
|
742
|
+
);
|
|
743
|
+
if (!trace) {
|
|
744
|
+
return { error: `No trace found matching ${traceIdPrefix}` };
|
|
745
|
+
}
|
|
746
|
+
const traceLogs = ctx.logs.filter((l) => l.traceId === trace.traceId);
|
|
747
|
+
return {
|
|
748
|
+
traceId: trace.traceId.slice(0, 16),
|
|
749
|
+
rootName: trace.rootName,
|
|
750
|
+
durationMs: Math.round(trace.durationMs),
|
|
751
|
+
hasError: trace.hasError,
|
|
752
|
+
spanCount: trace.spanCount,
|
|
753
|
+
spans: trace.spans.map((s) => ({
|
|
754
|
+
name: s.name,
|
|
755
|
+
durationMs: Math.round(s.durationMs),
|
|
756
|
+
status: s.status,
|
|
757
|
+
kind: s.kind,
|
|
758
|
+
parentSpanId: s.parentSpanId?.slice(0, 8),
|
|
759
|
+
attrs: Object.fromEntries(
|
|
760
|
+
Object.entries(s.attributes ?? {}).filter(
|
|
761
|
+
([k]) => [
|
|
762
|
+
"http.method",
|
|
763
|
+
"http.route",
|
|
764
|
+
"http.status_code",
|
|
765
|
+
"db.operation",
|
|
766
|
+
"db.system",
|
|
767
|
+
"service.name",
|
|
768
|
+
"error.message",
|
|
769
|
+
"error.type"
|
|
770
|
+
].includes(k)
|
|
771
|
+
)
|
|
772
|
+
)
|
|
773
|
+
})),
|
|
774
|
+
logs: traceLogs.slice(0, 10).map((l) => ({
|
|
775
|
+
level: l.level,
|
|
776
|
+
message: l.message.slice(0, 100)
|
|
777
|
+
}))
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
}),
|
|
781
|
+
searchSpans: t({
|
|
782
|
+
description: "Search spans by name pattern (case-insensitive substring match). Returns matching spans with details.",
|
|
783
|
+
parameters: zod.z.object({
|
|
784
|
+
query: zod.z.string().describe("Search string to match against span names"),
|
|
785
|
+
limit: zod.z.number().optional().describe("Max results (default 20)")
|
|
786
|
+
}),
|
|
787
|
+
execute: async ({ query, limit }) => {
|
|
788
|
+
const max = limit ?? 20;
|
|
789
|
+
const q = query.toLowerCase();
|
|
790
|
+
return ctx.spans.filter((s) => s.name.toLowerCase().includes(q)).slice(0, max).map((s) => ({
|
|
791
|
+
name: s.name,
|
|
792
|
+
durationMs: Math.round(s.durationMs),
|
|
793
|
+
status: s.status,
|
|
794
|
+
traceId: s.traceId.slice(0, 8),
|
|
795
|
+
service: s.attributes?.["service.name"] ?? "unknown"
|
|
796
|
+
}));
|
|
797
|
+
}
|
|
798
|
+
}),
|
|
799
|
+
searchLogs: t({
|
|
800
|
+
description: "Search logs by message content (case-insensitive). Returns matching log entries.",
|
|
801
|
+
parameters: zod.z.object({
|
|
802
|
+
query: zod.z.string().describe("Search string to match against log messages"),
|
|
803
|
+
level: zod.z.enum(["debug", "info", "warn", "error"]).optional().describe("Filter by log level"),
|
|
804
|
+
limit: zod.z.number().optional().describe("Max results (default 20)")
|
|
805
|
+
}),
|
|
806
|
+
execute: async ({
|
|
807
|
+
query,
|
|
808
|
+
level,
|
|
809
|
+
limit
|
|
810
|
+
}) => {
|
|
811
|
+
const max = limit ?? 20;
|
|
812
|
+
const q = query.toLowerCase();
|
|
813
|
+
let filtered = ctx.logs.filter(
|
|
814
|
+
(l) => l.message.toLowerCase().includes(q)
|
|
815
|
+
);
|
|
816
|
+
if (level) {
|
|
817
|
+
filtered = filtered.filter((l) => l.level === level);
|
|
818
|
+
}
|
|
819
|
+
return filtered.slice(0, max).map((l) => ({
|
|
820
|
+
level: l.level,
|
|
821
|
+
message: l.message.slice(0, 200),
|
|
822
|
+
traceId: l.traceId?.slice(0, 8),
|
|
823
|
+
attrs: l.attributes
|
|
824
|
+
}));
|
|
825
|
+
}
|
|
826
|
+
})
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// src/ai/stream.ts
|
|
831
|
+
async function providerStreamText(providerType, params) {
|
|
832
|
+
if (providerType === "ollama") {
|
|
833
|
+
const mod2 = await import('ai-sdk-ollama');
|
|
834
|
+
const result = await mod2.streamText(
|
|
835
|
+
params
|
|
836
|
+
);
|
|
837
|
+
return result;
|
|
838
|
+
}
|
|
839
|
+
const mod = await import('ai');
|
|
840
|
+
return mod.streamText(
|
|
841
|
+
params
|
|
842
|
+
);
|
|
843
|
+
}
|
|
531
844
|
var KEY_ATTR_KEYS = /* @__PURE__ */ new Set([
|
|
532
845
|
"http.route",
|
|
533
846
|
"http.method",
|
|
@@ -544,13 +857,15 @@ var THROTTLE_MS = 50;
|
|
|
544
857
|
var MAX_TRACES = 50;
|
|
545
858
|
var NEW_ERROR_DISPLAY_MS = 2e3;
|
|
546
859
|
var RECORD_LIMIT_DEFAULT = 200;
|
|
860
|
+
var LIST_HEIGHT = 20;
|
|
547
861
|
function Dashboard({
|
|
548
862
|
title,
|
|
549
863
|
showStats,
|
|
550
864
|
maxSpans,
|
|
551
865
|
colors,
|
|
552
866
|
stream,
|
|
553
|
-
logStream
|
|
867
|
+
logStream,
|
|
868
|
+
aiConfig
|
|
554
869
|
}) {
|
|
555
870
|
const [paused, setPaused] = react.useState(false);
|
|
556
871
|
const [recording, setRecording] = react.useState(false);
|
|
@@ -563,13 +878,43 @@ function Dashboard({
|
|
|
563
878
|
const [spanFilters, setSpanFilters] = react.useState({
|
|
564
879
|
statusGroup: "all"
|
|
565
880
|
});
|
|
566
|
-
const [
|
|
567
|
-
const [
|
|
881
|
+
const [drilldownTraceId, setDrilldownTraceId] = react.useState(null);
|
|
882
|
+
const [drilldownSelectedIndex, setDrilldownSelectedIndex] = react.useState(0);
|
|
883
|
+
const [drilldownScrollOffset, setDrilldownScrollOffset] = react.useState(0);
|
|
884
|
+
const [drilldownTab, setDrilldownTab] = react.useState("timeline");
|
|
568
885
|
const [newErrorCount, setNewErrorCount] = react.useState(0);
|
|
569
886
|
const [searchMode, setSearchMode] = react.useState(false);
|
|
887
|
+
const [traceIdMode, setTraceIdMode] = react.useState(false);
|
|
888
|
+
const [traceIdInput, setTraceIdInput] = react.useState("");
|
|
570
889
|
const throttleRef = react.useRef(null);
|
|
571
890
|
const pendingSpansRef = react.useRef([]);
|
|
572
891
|
const [logs, setLogs] = react.useState([]);
|
|
892
|
+
const [aiActive, setAiActive] = react.useState(false);
|
|
893
|
+
const [aiMessages, setAiMessages] = react.useState([]);
|
|
894
|
+
const [aiInput, setAiInput] = react.useState("");
|
|
895
|
+
const [aiState, setAiState] = react.useState({ status: "unconfigured" });
|
|
896
|
+
const [aiInputMode, setAiInputMode] = react.useState(false);
|
|
897
|
+
const aiModelRef = react.useRef(null);
|
|
898
|
+
const aiAbortRef = react.useRef(null);
|
|
899
|
+
react.useEffect(() => {
|
|
900
|
+
let cancelled = false;
|
|
901
|
+
resolveConfigWithAutoDetect(aiConfig).then(async (config) => {
|
|
902
|
+
if (cancelled || !config) return;
|
|
903
|
+
try {
|
|
904
|
+
const result = await createAIModel(config);
|
|
905
|
+
aiModelRef.current = result;
|
|
906
|
+
setAiState({ status: "idle" });
|
|
907
|
+
} catch {
|
|
908
|
+
setAiState({
|
|
909
|
+
status: "error",
|
|
910
|
+
message: "Failed to initialize AI model"
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
return () => {
|
|
915
|
+
cancelled = true;
|
|
916
|
+
};
|
|
917
|
+
}, [aiConfig]);
|
|
573
918
|
react.useEffect(() => {
|
|
574
919
|
if (!logStream) return;
|
|
575
920
|
const unsubscribe = logStream.onLog((event) => {
|
|
@@ -601,13 +946,16 @@ function Dashboard({
|
|
|
601
946
|
return next.slice(0, maxSpans);
|
|
602
947
|
});
|
|
603
948
|
setSelected(0);
|
|
604
|
-
|
|
949
|
+
setDrilldownTraceId(null);
|
|
605
950
|
};
|
|
606
951
|
const unsubscribe = stream.onSpanEnd((span) => {
|
|
607
952
|
if (paused) return;
|
|
608
953
|
if (span.status === "ERROR") {
|
|
609
954
|
setNewErrorCount((n) => n + 1);
|
|
610
|
-
setTimeout(
|
|
955
|
+
setTimeout(
|
|
956
|
+
() => setNewErrorCount((n) => Math.max(0, n - 1)),
|
|
957
|
+
NEW_ERROR_DISPLAY_MS
|
|
958
|
+
);
|
|
611
959
|
}
|
|
612
960
|
if (recording) {
|
|
613
961
|
setSpans((prev) => {
|
|
@@ -619,7 +967,7 @@ function Dashboard({
|
|
|
619
967
|
return next.slice(0, RECORD_LIMIT_DEFAULT);
|
|
620
968
|
});
|
|
621
969
|
setSelected(0);
|
|
622
|
-
|
|
970
|
+
setDrilldownTraceId(null);
|
|
623
971
|
return;
|
|
624
972
|
}
|
|
625
973
|
pendingSpansRef.current = [span, ...pendingSpansRef.current];
|
|
@@ -650,21 +998,24 @@ function Dashboard({
|
|
|
650
998
|
() => buildTraceSummaries(traceMap),
|
|
651
999
|
[traceMap]
|
|
652
1000
|
);
|
|
653
|
-
const filteredSummaries = react.useMemo(
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
);
|
|
1001
|
+
const filteredSummaries = react.useMemo(() => {
|
|
1002
|
+
const base = filterTraceSummaries(traceSummaries, "");
|
|
1003
|
+
return spanFilters.traceId ? base.filter((t2) => t2.traceId === spanFilters.traceId) : base;
|
|
1004
|
+
}, [traceSummaries, spanFilters.traceId]);
|
|
657
1005
|
const filteredSpans = react.useMemo(
|
|
658
1006
|
() => filterBySearch(filteredSpanBuffer, ""),
|
|
659
1007
|
[filteredSpanBuffer]
|
|
660
1008
|
);
|
|
661
1009
|
const stats = react.useMemo(() => computeStats(spans), [spans]);
|
|
662
|
-
const perSpanNameStats = react.useMemo(
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
() => filterLogsBySearch(logs, searchQuery),
|
|
666
|
-
[logs, searchQuery]
|
|
1010
|
+
const perSpanNameStats = react.useMemo(
|
|
1011
|
+
() => computePerSpanNameStats(spans),
|
|
1012
|
+
[spans]
|
|
667
1013
|
);
|
|
1014
|
+
const logStats = react.useMemo(() => computeLogStats(logs), [logs]);
|
|
1015
|
+
const filteredLogs = react.useMemo(() => {
|
|
1016
|
+
const traceFilteredLogs = spanFilters.traceId ? logs.filter((l) => l.traceId === spanFilters.traceId) : logs;
|
|
1017
|
+
return filterLogsBySearch(traceFilteredLogs, searchQuery);
|
|
1018
|
+
}, [logs, searchQuery, spanFilters.traceId]);
|
|
668
1019
|
const serviceStats = react.useMemo(() => computeServiceStats(spans), [spans]);
|
|
669
1020
|
const selectedServiceName = serviceStats[selected]?.serviceName ?? null;
|
|
670
1021
|
const spansForSelectedService = react.useMemo(() => {
|
|
@@ -681,24 +1032,147 @@ function Dashboard({
|
|
|
681
1032
|
() => findHotSpanNames(spansForSelectedService, 8),
|
|
682
1033
|
[spansForSelectedService]
|
|
683
1034
|
);
|
|
684
|
-
const selectedTraceSummary =
|
|
1035
|
+
const selectedTraceSummary = drilldownTraceId == null ? filteredSummaries[selected] ?? null : filteredSummaries.find((t2) => t2.traceId === drilldownTraceId) ?? null;
|
|
685
1036
|
const errorSummaries = react.useMemo(
|
|
686
1037
|
() => buildErrorSummaries(traceSummaries),
|
|
687
1038
|
[traceSummaries]
|
|
688
1039
|
);
|
|
1040
|
+
const filteredErrorSummaries = spanFilters.traceId ? errorSummaries.filter((e) => e.traceId === spanFilters.traceId) : errorSummaries;
|
|
689
1041
|
const traceTree = selectedTraceSummary == null ? [] : flattenTraceTree(buildTraceTree(selectedTraceSummary.spans));
|
|
690
1042
|
const waterfallSpans = selectedTraceSummary == null ? [] : sortSpansForWaterfall(selectedTraceSummary.spans);
|
|
691
|
-
const currentSpanInTrace = traceTree[
|
|
1043
|
+
const currentSpanInTrace = traceTree[drilldownSelectedIndex] ?? null;
|
|
692
1044
|
const currentSpanInFlat = filteredSpans[selected] ?? null;
|
|
693
|
-
const selectedTraceSummaryForDetails = viewMode === "trace" &&
|
|
1045
|
+
const selectedTraceSummaryForDetails = viewMode === "trace" && drilldownTraceId == null && filteredSummaries[selected] ? filteredSummaries[selected] : null;
|
|
694
1046
|
const rootSpanOfSelectedTrace = selectedTraceSummaryForDetails != null && selectedTraceSummaryForDetails.spans.length > 0 ? selectedTraceSummaryForDetails.spans.find(
|
|
695
1047
|
(s) => !selectedTraceSummaryForDetails.spans.some(
|
|
696
1048
|
(p) => p.spanId === s.parentSpanId
|
|
697
1049
|
)
|
|
698
1050
|
) ?? selectedTraceSummaryForDetails.spans[0] : null;
|
|
699
|
-
const currentSpan = viewMode === "trace" ?
|
|
1051
|
+
const currentSpan = viewMode === "trace" ? drilldownTraceId == null ? rootSpanOfSelectedTrace ?? null : currentSpanInTrace?.span ?? null : viewMode === "span" ? currentSpanInFlat : null;
|
|
700
1052
|
const selectedTraceLogs = selectedTraceSummary?.traceId && logs.length > 0 ? logs.filter((l) => l.traceId === selectedTraceSummary.traceId) : [];
|
|
701
1053
|
const timelineItems = selectedTraceSummary && (selectedTraceSummary.spans.length > 0 || selectedTraceLogs.length > 0) ? buildTraceTimeline(selectedTraceSummary.spans, selectedTraceLogs) : [];
|
|
1054
|
+
const drilldownSpans = react.useMemo(
|
|
1055
|
+
() => drilldownTraceId ? spans.filter((s) => s.traceId === drilldownTraceId) : [],
|
|
1056
|
+
[spans, drilldownTraceId]
|
|
1057
|
+
);
|
|
1058
|
+
const drilldownTree = react.useMemo(
|
|
1059
|
+
() => drilldownSpans.length > 0 ? flattenTraceTree(buildTraceTree(drilldownSpans)) : [],
|
|
1060
|
+
[drilldownSpans]
|
|
1061
|
+
);
|
|
1062
|
+
const drilldownLogs = react.useMemo(
|
|
1063
|
+
() => drilldownTraceId ? logs.filter((l) => l.traceId === drilldownTraceId) : [],
|
|
1064
|
+
[logs, drilldownTraceId]
|
|
1065
|
+
);
|
|
1066
|
+
const drilldownTimeline = react.useMemo(
|
|
1067
|
+
() => drilldownSpans.length > 0 || drilldownLogs.length > 0 ? buildTraceTimeline(drilldownSpans, drilldownLogs) : [],
|
|
1068
|
+
[drilldownSpans, drilldownLogs]
|
|
1069
|
+
);
|
|
1070
|
+
const drilldownSummary = react.useMemo(
|
|
1071
|
+
() => drilldownTraceId ? traceSummaries.find((t2) => t2.traceId === drilldownTraceId) ?? null : null,
|
|
1072
|
+
[traceSummaries, drilldownTraceId]
|
|
1073
|
+
);
|
|
1074
|
+
const drilldownSelectedItem = react.useMemo(() => {
|
|
1075
|
+
if (!drilldownTraceId) return null;
|
|
1076
|
+
if (drilldownTab === "timeline") {
|
|
1077
|
+
const item = drilldownTimeline[drilldownSelectedIndex];
|
|
1078
|
+
if (!item) return null;
|
|
1079
|
+
return item;
|
|
1080
|
+
}
|
|
1081
|
+
if (drilldownTab === "spans") {
|
|
1082
|
+
return drilldownTree[drilldownSelectedIndex] ? {
|
|
1083
|
+
type: "span",
|
|
1084
|
+
span: drilldownTree[drilldownSelectedIndex].span
|
|
1085
|
+
} : null;
|
|
1086
|
+
}
|
|
1087
|
+
if (drilldownTab === "logs") {
|
|
1088
|
+
return drilldownLogs[drilldownSelectedIndex] ? { type: "log", log: drilldownLogs[drilldownSelectedIndex] } : null;
|
|
1089
|
+
}
|
|
1090
|
+
return null;
|
|
1091
|
+
}, [
|
|
1092
|
+
drilldownTraceId,
|
|
1093
|
+
drilldownTab,
|
|
1094
|
+
drilldownSelectedIndex,
|
|
1095
|
+
drilldownTimeline,
|
|
1096
|
+
drilldownTree,
|
|
1097
|
+
drilldownLogs
|
|
1098
|
+
]);
|
|
1099
|
+
const sendAIQuery = async (question) => {
|
|
1100
|
+
const aiResult = aiModelRef.current;
|
|
1101
|
+
if (!aiResult || aiState.status === "streaming") return;
|
|
1102
|
+
const userMsg = { role: "user", content: question };
|
|
1103
|
+
setAiMessages((prev) => [...prev, userMsg]);
|
|
1104
|
+
setAiInput("");
|
|
1105
|
+
const abort = new AbortController();
|
|
1106
|
+
aiAbortRef.current = abort;
|
|
1107
|
+
setAiState({ status: "streaming", abortController: abort });
|
|
1108
|
+
const toolCtx = {
|
|
1109
|
+
spans,
|
|
1110
|
+
logs,
|
|
1111
|
+
traces: traceSummaries,
|
|
1112
|
+
stats,
|
|
1113
|
+
serviceStats,
|
|
1114
|
+
errorSummaries
|
|
1115
|
+
};
|
|
1116
|
+
const tools = createTelemetryTools(toolCtx);
|
|
1117
|
+
const statsContext = JSON.stringify({
|
|
1118
|
+
viewMode,
|
|
1119
|
+
stats: {
|
|
1120
|
+
totalSpans: stats.total,
|
|
1121
|
+
errors: stats.errors,
|
|
1122
|
+
avgMs: Math.round(stats.avg),
|
|
1123
|
+
p95Ms: Math.round(stats.p95)
|
|
1124
|
+
},
|
|
1125
|
+
services: serviceStats.length,
|
|
1126
|
+
traces: traceSummaries.length
|
|
1127
|
+
});
|
|
1128
|
+
const drilldownContext = drilldownTraceId ? `
|
|
1129
|
+
|
|
1130
|
+
Currently viewing trace ${drilldownTraceId}. This trace has ${drilldownSpans.length} spans and ${drilldownLogs.length} logs. The root span is "${drilldownSummary?.rootName ?? "unknown"}" with duration ${drilldownSummary ? formatDurationMs(drilldownSummary.durationMs) : "unknown"}.` : "";
|
|
1131
|
+
const systemPrompt = buildSystemPrompt(viewMode, statsContext) + drilldownContext;
|
|
1132
|
+
try {
|
|
1133
|
+
setAiMessages((prev) => [...prev, { role: "assistant", content: "" }]);
|
|
1134
|
+
const result = await providerStreamText(aiResult.providerType, {
|
|
1135
|
+
model: aiResult.model,
|
|
1136
|
+
system: systemPrompt,
|
|
1137
|
+
messages: [...aiMessages, userMsg].map((m) => ({
|
|
1138
|
+
role: m.role,
|
|
1139
|
+
content: m.content
|
|
1140
|
+
})),
|
|
1141
|
+
tools,
|
|
1142
|
+
maxSteps: 10,
|
|
1143
|
+
abortSignal: abort.signal
|
|
1144
|
+
});
|
|
1145
|
+
let fullText = "";
|
|
1146
|
+
for await (const chunk of result.textStream) {
|
|
1147
|
+
if (abort.signal.aborted) break;
|
|
1148
|
+
fullText += chunk;
|
|
1149
|
+
const captured = fullText;
|
|
1150
|
+
setAiMessages((prev) => {
|
|
1151
|
+
const updated = [...prev];
|
|
1152
|
+
const lastMsg = updated.at(-1);
|
|
1153
|
+
if (lastMsg?.role === "assistant") {
|
|
1154
|
+
updated[updated.length - 1] = {
|
|
1155
|
+
role: "assistant",
|
|
1156
|
+
content: captured
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
return updated;
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
setAiState({ status: "idle" });
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
if (abort.signal.aborted) {
|
|
1165
|
+
setAiState({ status: "idle" });
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
setAiState({
|
|
1169
|
+
status: "error",
|
|
1170
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1171
|
+
});
|
|
1172
|
+
} finally {
|
|
1173
|
+
aiAbortRef.current = null;
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
702
1176
|
const { isRawModeSupported } = ink.useStdin();
|
|
703
1177
|
ink.useInput(
|
|
704
1178
|
(input, key) => {
|
|
@@ -716,53 +1190,184 @@ function Dashboard({
|
|
|
716
1190
|
return;
|
|
717
1191
|
}
|
|
718
1192
|
if (searchMode) {
|
|
719
|
-
if (key.
|
|
720
|
-
setSearchQuery((q) => q.slice(0, -1));
|
|
721
|
-
} else if (key.return) {
|
|
1193
|
+
if (key.escape || key.return) {
|
|
722
1194
|
setSearchMode(false);
|
|
1195
|
+
} else if (key.backspace || key.delete) {
|
|
1196
|
+
setSearchQuery((q) => q.slice(0, -1));
|
|
1197
|
+
} else if (key.tab) {
|
|
1198
|
+
if (searchQuery.length >= 4) {
|
|
1199
|
+
const match = traceSummaries.find(
|
|
1200
|
+
(t2) => t2.traceId.toLowerCase().startsWith(searchQuery.toLowerCase())
|
|
1201
|
+
);
|
|
1202
|
+
if (match) {
|
|
1203
|
+
setSpanFilters((prev) => ({ ...prev, traceId: match.traceId }));
|
|
1204
|
+
setSearchMode(false);
|
|
1205
|
+
setSearchQuery("");
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
723
1208
|
} else if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
724
1209
|
setSearchQuery((q) => q + input);
|
|
725
1210
|
}
|
|
726
1211
|
return;
|
|
727
1212
|
}
|
|
728
|
-
if (
|
|
729
|
-
if (
|
|
730
|
-
|
|
731
|
-
|
|
1213
|
+
if (traceIdMode) {
|
|
1214
|
+
if (key.escape) {
|
|
1215
|
+
setTraceIdMode(false);
|
|
1216
|
+
setTraceIdInput("");
|
|
1217
|
+
} else if (key.return) {
|
|
1218
|
+
if (traceIdInput.trim()) {
|
|
1219
|
+
const match = traceSummaries.find(
|
|
1220
|
+
(t2) => t2.traceId.toLowerCase().startsWith(traceIdInput.toLowerCase())
|
|
1221
|
+
);
|
|
1222
|
+
if (match) {
|
|
1223
|
+
setSpanFilters((prev) => ({ ...prev, traceId: match.traceId }));
|
|
1224
|
+
}
|
|
1225
|
+
} else {
|
|
1226
|
+
setSpanFilters((prev) => {
|
|
1227
|
+
const { traceId: _, ...rest } = prev;
|
|
1228
|
+
return rest;
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
setTraceIdMode(false);
|
|
1232
|
+
setTraceIdInput("");
|
|
1233
|
+
} else if (key.tab) {
|
|
1234
|
+
if (traceIdInput.length >= 2) {
|
|
1235
|
+
const match = traceSummaries.find(
|
|
1236
|
+
(t2) => t2.traceId.toLowerCase().startsWith(traceIdInput.toLowerCase())
|
|
1237
|
+
);
|
|
1238
|
+
if (match) {
|
|
1239
|
+
setTraceIdInput(match.traceId);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
} else if (key.backspace || key.delete) {
|
|
1243
|
+
setTraceIdInput((q) => q.slice(0, -1));
|
|
1244
|
+
} else if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
1245
|
+
setTraceIdInput((q) => q + input);
|
|
1246
|
+
}
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
if (aiInputMode) {
|
|
1250
|
+
if (key.escape) {
|
|
1251
|
+
if (aiState.status === "streaming") {
|
|
1252
|
+
aiAbortRef.current?.abort();
|
|
1253
|
+
} else {
|
|
1254
|
+
setAiInputMode(false);
|
|
1255
|
+
setAiActive(false);
|
|
1256
|
+
}
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
if (key.backspace || key.delete) {
|
|
1260
|
+
setAiInput((q) => q.slice(0, -1));
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
if (key.return) {
|
|
1264
|
+
if (aiInput.trim()) {
|
|
1265
|
+
sendAIQuery(aiInput.trim());
|
|
1266
|
+
}
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
1270
|
+
setAiInput((q) => q + input);
|
|
1271
|
+
}
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
if (input === "a") {
|
|
1275
|
+
setAiActive((v) => !v);
|
|
1276
|
+
if (aiActive) {
|
|
1277
|
+
setAiInputMode(false);
|
|
732
1278
|
} else {
|
|
1279
|
+
if (aiState.status !== "unconfigured") {
|
|
1280
|
+
setAiInputMode(true);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
if (key.escape) {
|
|
1286
|
+
if (drilldownTraceId == null) {
|
|
733
1287
|
setSearchMode(false);
|
|
1288
|
+
} else {
|
|
1289
|
+
setDrilldownTraceId(null);
|
|
1290
|
+
setDrilldownSelectedIndex(0);
|
|
1291
|
+
setDrilldownScrollOffset(0);
|
|
1292
|
+
setDrilldownTab("timeline");
|
|
734
1293
|
}
|
|
735
1294
|
return;
|
|
736
1295
|
}
|
|
737
|
-
if (key.
|
|
738
|
-
|
|
739
|
-
|
|
1296
|
+
if (key.tab && drilldownTraceId != null) {
|
|
1297
|
+
const tabs = [
|
|
1298
|
+
"timeline",
|
|
1299
|
+
"spans",
|
|
1300
|
+
"logs"
|
|
1301
|
+
];
|
|
1302
|
+
const currentIdx = tabs.indexOf(drilldownTab);
|
|
1303
|
+
const nextIdx = key.shift ? (currentIdx - 1 + tabs.length) % tabs.length : (currentIdx + 1) % tabs.length;
|
|
1304
|
+
setDrilldownTab(tabs[nextIdx]);
|
|
1305
|
+
setDrilldownSelectedIndex(0);
|
|
1306
|
+
setDrilldownScrollOffset(0);
|
|
740
1307
|
return;
|
|
741
1308
|
}
|
|
1309
|
+
if (key.return && drilldownTraceId == null) {
|
|
1310
|
+
let targetTraceId;
|
|
1311
|
+
let preSelectIndex = 0;
|
|
1312
|
+
if (viewMode === "trace" && filteredSummaries[selected]) {
|
|
1313
|
+
targetTraceId = filteredSummaries[selected].traceId;
|
|
1314
|
+
} else if (viewMode === "log" && filteredLogs[selected]?.traceId) {
|
|
1315
|
+
targetTraceId = filteredLogs[selected].traceId;
|
|
1316
|
+
const originLog = filteredLogs[selected];
|
|
1317
|
+
const traceSpans = spans.filter((s) => s.traceId === targetTraceId);
|
|
1318
|
+
const traceLogs = logs.filter((l) => l.traceId === targetTraceId);
|
|
1319
|
+
const timeline = buildTraceTimeline(traceSpans, traceLogs);
|
|
1320
|
+
preSelectIndex = timeline.findIndex(
|
|
1321
|
+
(item) => item.type === "log" && item.log === originLog
|
|
1322
|
+
);
|
|
1323
|
+
if (preSelectIndex < 0) preSelectIndex = 0;
|
|
1324
|
+
} else if (viewMode === "span" && filteredSpans[selected]?.traceId) {
|
|
1325
|
+
targetTraceId = filteredSpans[selected].traceId;
|
|
1326
|
+
const originSpan = filteredSpans[selected];
|
|
1327
|
+
const traceSpans = spans.filter((s) => s.traceId === targetTraceId);
|
|
1328
|
+
const traceLogs = logs.filter((l) => l.traceId === targetTraceId);
|
|
1329
|
+
const timeline = buildTraceTimeline(traceSpans, traceLogs);
|
|
1330
|
+
preSelectIndex = timeline.findIndex(
|
|
1331
|
+
(item) => item.type === "span" && item.span?.spanId === originSpan.spanId
|
|
1332
|
+
);
|
|
1333
|
+
if (preSelectIndex < 0) preSelectIndex = 0;
|
|
1334
|
+
}
|
|
1335
|
+
if (targetTraceId) {
|
|
1336
|
+
setDrilldownTraceId(targetTraceId);
|
|
1337
|
+
setDrilldownSelectedIndex(preSelectIndex);
|
|
1338
|
+
setDrilldownScrollOffset(Math.max(0, preSelectIndex - LIST_HEIGHT + 1));
|
|
1339
|
+
setDrilldownTab("timeline");
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
742
1343
|
if (key.upArrow || key.downArrow) {
|
|
1344
|
+
if (drilldownTraceId != null) {
|
|
1345
|
+
const listLength = drilldownTab === "timeline" ? drilldownTimeline.length : drilldownTab === "spans" ? drilldownTree.length : drilldownLogs.length;
|
|
1346
|
+
if (key.upArrow) {
|
|
1347
|
+
setDrilldownSelectedIndex((prev) => {
|
|
1348
|
+
const next = Math.max(0, prev - 1);
|
|
1349
|
+
setDrilldownScrollOffset((off) => Math.min(next, off));
|
|
1350
|
+
return next;
|
|
1351
|
+
});
|
|
1352
|
+
} else {
|
|
1353
|
+
setDrilldownSelectedIndex((prev) => {
|
|
1354
|
+
const next = Math.min(listLength - 1, prev + 1);
|
|
1355
|
+
setDrilldownScrollOffset(
|
|
1356
|
+
(off) => next >= off + LIST_HEIGHT ? next - LIST_HEIGHT + 1 : off
|
|
1357
|
+
);
|
|
1358
|
+
return next;
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
743
1363
|
switch (viewMode) {
|
|
744
1364
|
case "trace": {
|
|
745
1365
|
if (key.upArrow) {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
} else {
|
|
749
|
-
setSelected((i) => Math.max(0, i - 1));
|
|
750
|
-
setSelectedSpanIndex(0);
|
|
751
|
-
}
|
|
1366
|
+
setSelected((i) => Math.max(0, i - 1));
|
|
1367
|
+
setDrilldownSelectedIndex(0);
|
|
752
1368
|
} else if (key.downArrow) {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
} else if (selectedTraceId != null && traceTree.length > 0 && selectedSpanIndex >= traceTree.length - 1) {
|
|
756
|
-
const nextIdx = filteredSummaries.findIndex((t) => t.traceId === selectedTraceId) + 1;
|
|
757
|
-
if (nextIdx < filteredSummaries.length) {
|
|
758
|
-
setSelected(nextIdx);
|
|
759
|
-
setSelectedTraceId(filteredSummaries[nextIdx].traceId);
|
|
760
|
-
setSelectedSpanIndex(0);
|
|
761
|
-
}
|
|
762
|
-
} else if (selectedTraceId == null) {
|
|
763
|
-
setSelected((i) => Math.min(filteredSummaries.length - 1, i + 1));
|
|
764
|
-
setSelectedSpanIndex(0);
|
|
765
|
-
}
|
|
1369
|
+
setSelected((i) => Math.min(filteredSummaries.length - 1, i + 1));
|
|
1370
|
+
setDrilldownSelectedIndex(0);
|
|
766
1371
|
}
|
|
767
1372
|
break;
|
|
768
1373
|
}
|
|
@@ -794,7 +1399,9 @@ function Dashboard({
|
|
|
794
1399
|
if (key.upArrow) {
|
|
795
1400
|
setSelected((i) => Math.max(0, i - 1));
|
|
796
1401
|
} else if (key.downArrow) {
|
|
797
|
-
setSelected(
|
|
1402
|
+
setSelected(
|
|
1403
|
+
(i) => Math.min(filteredErrorSummaries.length - 1, i + 1)
|
|
1404
|
+
);
|
|
798
1405
|
}
|
|
799
1406
|
break;
|
|
800
1407
|
}
|
|
@@ -805,35 +1412,40 @@ function Dashboard({
|
|
|
805
1412
|
if (input === "t") {
|
|
806
1413
|
setViewMode((m) => m === "trace" ? "span" : "trace");
|
|
807
1414
|
setSelected(0);
|
|
808
|
-
|
|
809
|
-
|
|
1415
|
+
setDrilldownTraceId(null);
|
|
1416
|
+
setDrilldownSelectedIndex(0);
|
|
1417
|
+
setDrilldownScrollOffset(0);
|
|
810
1418
|
}
|
|
811
1419
|
if (input === "l") {
|
|
812
1420
|
setViewMode((m) => m === "log" ? "trace" : "log");
|
|
813
1421
|
setSelected(0);
|
|
814
|
-
|
|
815
|
-
|
|
1422
|
+
setDrilldownTraceId(null);
|
|
1423
|
+
setDrilldownSelectedIndex(0);
|
|
1424
|
+
setDrilldownScrollOffset(0);
|
|
816
1425
|
}
|
|
817
1426
|
if (input === "v") {
|
|
818
1427
|
setViewMode(
|
|
819
1428
|
(m) => m === "service-summary" ? "trace" : "service-summary"
|
|
820
1429
|
);
|
|
821
1430
|
setSelected(0);
|
|
822
|
-
|
|
823
|
-
|
|
1431
|
+
setDrilldownTraceId(null);
|
|
1432
|
+
setDrilldownSelectedIndex(0);
|
|
1433
|
+
setDrilldownScrollOffset(0);
|
|
824
1434
|
}
|
|
825
1435
|
if (input === "E") {
|
|
826
1436
|
setViewMode((m) => m === "errors" ? "trace" : "errors");
|
|
827
1437
|
setSelected(0);
|
|
828
|
-
|
|
829
|
-
|
|
1438
|
+
setDrilldownTraceId(null);
|
|
1439
|
+
setDrilldownSelectedIndex(0);
|
|
1440
|
+
setDrilldownScrollOffset(0);
|
|
830
1441
|
}
|
|
831
1442
|
if (input === "c") {
|
|
832
1443
|
setSpans([]);
|
|
833
1444
|
setLogs([]);
|
|
834
1445
|
setSelected(0);
|
|
835
|
-
|
|
836
|
-
|
|
1446
|
+
setDrilldownTraceId(null);
|
|
1447
|
+
setDrilldownSelectedIndex(0);
|
|
1448
|
+
setDrilldownScrollOffset(0);
|
|
837
1449
|
setNewErrorCount(0);
|
|
838
1450
|
setSpanFilters({ statusGroup: "all" });
|
|
839
1451
|
setRecording(false);
|
|
@@ -843,8 +1455,9 @@ function Dashboard({
|
|
|
843
1455
|
setSpans([]);
|
|
844
1456
|
setLogs([]);
|
|
845
1457
|
setSelected(0);
|
|
846
|
-
|
|
847
|
-
|
|
1458
|
+
setDrilldownTraceId(null);
|
|
1459
|
+
setDrilldownSelectedIndex(0);
|
|
1460
|
+
setDrilldownScrollOffset(0);
|
|
848
1461
|
setNewErrorCount(0);
|
|
849
1462
|
setSpanFilters({ statusGroup: "all" });
|
|
850
1463
|
setPaused(false);
|
|
@@ -853,8 +1466,14 @@ function Dashboard({
|
|
|
853
1466
|
if (input === "x") {
|
|
854
1467
|
setSpanFilters({ statusGroup: "all" });
|
|
855
1468
|
setSelected(0);
|
|
856
|
-
|
|
857
|
-
|
|
1469
|
+
setDrilldownTraceId(null);
|
|
1470
|
+
setDrilldownSelectedIndex(0);
|
|
1471
|
+
setDrilldownScrollOffset(0);
|
|
1472
|
+
}
|
|
1473
|
+
if (input === "f") {
|
|
1474
|
+
setTraceIdMode(true);
|
|
1475
|
+
setTraceIdInput(spanFilters.traceId ?? "");
|
|
1476
|
+
return;
|
|
858
1477
|
}
|
|
859
1478
|
if (input === "H") {
|
|
860
1479
|
setSpanFilters((prev) => {
|
|
@@ -862,16 +1481,18 @@ function Dashboard({
|
|
|
862
1481
|
return { ...prev, statusGroup: next };
|
|
863
1482
|
});
|
|
864
1483
|
setSelected(0);
|
|
865
|
-
|
|
866
|
-
|
|
1484
|
+
setDrilldownTraceId(null);
|
|
1485
|
+
setDrilldownSelectedIndex(0);
|
|
1486
|
+
setDrilldownScrollOffset(0);
|
|
867
1487
|
}
|
|
868
1488
|
if (input === "S") {
|
|
869
1489
|
const svc = currentSpan?.attributes?.["service.name"];
|
|
870
1490
|
if (typeof svc === "string" && svc.trim()) {
|
|
871
1491
|
setSpanFilters((prev) => ({ ...prev, serviceName: svc }));
|
|
872
1492
|
setSelected(0);
|
|
873
|
-
|
|
874
|
-
|
|
1493
|
+
setDrilldownTraceId(null);
|
|
1494
|
+
setDrilldownSelectedIndex(0);
|
|
1495
|
+
setDrilldownScrollOffset(0);
|
|
875
1496
|
}
|
|
876
1497
|
}
|
|
877
1498
|
if (input === "R") {
|
|
@@ -879,14 +1500,77 @@ function Dashboard({
|
|
|
879
1500
|
if (typeof route === "string" && route.trim()) {
|
|
880
1501
|
setSpanFilters((prev) => ({ ...prev, route }));
|
|
881
1502
|
setSelected(0);
|
|
882
|
-
|
|
883
|
-
|
|
1503
|
+
setDrilldownTraceId(null);
|
|
1504
|
+
setDrilldownSelectedIndex(0);
|
|
1505
|
+
setDrilldownScrollOffset(0);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
if (input === "T") {
|
|
1509
|
+
let traceId;
|
|
1510
|
+
if (drilldownTraceId) {
|
|
1511
|
+
traceId = drilldownTraceId;
|
|
1512
|
+
} else {
|
|
1513
|
+
switch (viewMode) {
|
|
1514
|
+
case "log": {
|
|
1515
|
+
traceId = filteredLogs[selected]?.traceId;
|
|
1516
|
+
break;
|
|
1517
|
+
}
|
|
1518
|
+
case "span": {
|
|
1519
|
+
traceId = filteredSpans[selected]?.traceId;
|
|
1520
|
+
break;
|
|
1521
|
+
}
|
|
1522
|
+
case "errors": {
|
|
1523
|
+
traceId = filteredErrorSummaries[selected]?.traceId;
|
|
1524
|
+
break;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
if (traceId && viewMode !== "trace") {
|
|
1529
|
+
setSpanFilters((prev) => ({ ...prev, traceId }));
|
|
1530
|
+
setViewMode("trace");
|
|
1531
|
+
setSelected(0);
|
|
1532
|
+
setDrilldownTraceId(null);
|
|
1533
|
+
setDrilldownSelectedIndex(0);
|
|
1534
|
+
setDrilldownScrollOffset(0);
|
|
1535
|
+
setDrilldownTab("timeline");
|
|
1536
|
+
}
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
if (input === "L") {
|
|
1540
|
+
let traceId;
|
|
1541
|
+
if (drilldownTraceId) {
|
|
1542
|
+
traceId = drilldownTraceId;
|
|
1543
|
+
} else {
|
|
1544
|
+
switch (viewMode) {
|
|
1545
|
+
case "trace": {
|
|
1546
|
+
traceId = filteredSummaries[selected]?.traceId;
|
|
1547
|
+
break;
|
|
1548
|
+
}
|
|
1549
|
+
case "span": {
|
|
1550
|
+
traceId = filteredSpans[selected]?.traceId;
|
|
1551
|
+
break;
|
|
1552
|
+
}
|
|
1553
|
+
case "errors": {
|
|
1554
|
+
traceId = filteredErrorSummaries[selected]?.traceId;
|
|
1555
|
+
break;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
if (traceId && viewMode !== "log") {
|
|
1560
|
+
setSpanFilters((prev) => ({ ...prev, traceId }));
|
|
1561
|
+
setViewMode("log");
|
|
1562
|
+
setSelected(0);
|
|
1563
|
+
setDrilldownTraceId(null);
|
|
1564
|
+
setDrilldownSelectedIndex(0);
|
|
1565
|
+
setDrilldownScrollOffset(0);
|
|
1566
|
+
setDrilldownTab("timeline");
|
|
884
1567
|
}
|
|
1568
|
+
return;
|
|
885
1569
|
}
|
|
886
1570
|
if (input === "J") {
|
|
887
|
-
const
|
|
888
|
-
if (!
|
|
889
|
-
const json = exportTraceToJson(
|
|
1571
|
+
const t2 = selectedTraceSummary;
|
|
1572
|
+
if (!t2) return;
|
|
1573
|
+
const json = exportTraceToJson(t2, selectedTraceLogs);
|
|
890
1574
|
process.stdout.write(`
|
|
891
1575
|
[autotel-terminal] trace export
|
|
892
1576
|
${json}
|
|
@@ -899,22 +1583,32 @@ ${json}
|
|
|
899
1583
|
const headerModeLabel = viewMode === "trace" ? "traces" : viewMode === "span" ? "spans" : viewMode === "log" ? "logs" : viewMode === "service-summary" ? "services" : "errors";
|
|
900
1584
|
const showNewError = newErrorCount > 0;
|
|
901
1585
|
function renderTreeRow(node, index) {
|
|
902
|
-
const isSel =
|
|
1586
|
+
const isSel = drilldownTraceId != null && index === drilldownSelectedIndex;
|
|
903
1587
|
const prefix = node.depth === 0 ? "" : " ".repeat(node.depth) + (node.children.length > 0 ? "\u251C\u2500\u2500 " : "\u2514\u2500\u2500 ");
|
|
904
1588
|
const statusColor = node.span.status === "ERROR" ? "red" : node.span.durationMs > 500 ? "yellow" : "green";
|
|
905
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1589
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1590
|
+
ink.Box,
|
|
1591
|
+
{
|
|
1592
|
+
flexDirection: "row",
|
|
1593
|
+
children: [
|
|
1594
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
|
|
1595
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: prefix }),
|
|
1596
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: colors ? statusColor : void 0, children: truncate(node.span.name, 24) }),
|
|
1597
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1598
|
+
" ",
|
|
1599
|
+
formatDurationMs(node.span.durationMs)
|
|
1600
|
+
] })
|
|
1601
|
+
]
|
|
1602
|
+
},
|
|
1603
|
+
`${node.span.spanId}-${node.span.startTime}`
|
|
1604
|
+
);
|
|
914
1605
|
}
|
|
915
1606
|
function keyAttrsAndRest(attrs) {
|
|
916
1607
|
if (!attrs || Object.keys(attrs).length === 0)
|
|
917
|
-
return {
|
|
1608
|
+
return {
|
|
1609
|
+
key: [],
|
|
1610
|
+
rest: []
|
|
1611
|
+
};
|
|
918
1612
|
const entries = Object.entries(attrs);
|
|
919
1613
|
const key = entries.filter(([k]) => KEY_ATTR_KEYS.has(k));
|
|
920
1614
|
const rest = entries.filter(([k]) => !KEY_ATTR_KEYS.has(k));
|
|
@@ -926,9 +1620,7 @@ ${json}
|
|
|
926
1620
|
ink.Box,
|
|
927
1621
|
{
|
|
928
1622
|
flexDirection: "column",
|
|
929
|
-
|
|
930
|
-
padding: 1,
|
|
931
|
-
borderColor: colors ? "cyan" : void 0,
|
|
1623
|
+
paddingX: 1,
|
|
932
1624
|
children: [
|
|
933
1625
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { justifyContent: "space-between", marginBottom: 1, children: [
|
|
934
1626
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { bold: true, children: [
|
|
@@ -942,37 +1634,28 @@ ${json}
|
|
|
942
1634
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: paused ? "yellow" : "green", children: headerRight }, "status")
|
|
943
1635
|
] })
|
|
944
1636
|
] }),
|
|
945
|
-
|
|
946
|
-
ink.Box,
|
|
947
|
-
{
|
|
948
|
-
flexDirection: "column",
|
|
949
|
-
borderStyle: "single",
|
|
950
|
-
borderColor: "gray",
|
|
951
|
-
paddingX: 1,
|
|
952
|
-
marginBottom: 1,
|
|
953
|
-
children: [
|
|
954
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Shortcuts" }),
|
|
955
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Navigation: \u2191/\u2193, Enter, Esc" }),
|
|
956
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Views: t (trace/spans), l (logs), v (services), E (errors)" }),
|
|
957
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Search: /" }),
|
|
958
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Filters: e (errors-only), S (service), R (route), H (status), x (clear)" }),
|
|
959
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Capture: p (pause), r (record snapshot), J (export trace JSON)" }),
|
|
960
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Other: c (clear), ? (help), Ctrl+C (exit)" })
|
|
961
|
-
]
|
|
962
|
-
}
|
|
963
|
-
) : /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 1, flexDirection: "row", justifyContent: "space-between", children: [
|
|
1637
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 0, flexDirection: "column", children: [
|
|
964
1638
|
searchMode ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "cyan", children: [
|
|
965
1639
|
"Search: ",
|
|
966
|
-
searchQuery || "(type to filter)"
|
|
967
|
-
|
|
968
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1640
|
+
searchQuery || "(type to filter)",
|
|
1641
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: " (Tab: match traceId, Esc: cancel)" })
|
|
1642
|
+
] }) : traceIdMode ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "yellow", children: [
|
|
1643
|
+
"TraceId: ",
|
|
1644
|
+
traceIdInput || "(type prefix, Tab to complete)",
|
|
1645
|
+
traceIdInput.length >= 2 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1646
|
+
" \u2192 ",
|
|
1647
|
+
traceSummaries.find(
|
|
1648
|
+
(t2) => t2.traceId.toLowerCase().startsWith(traceIdInput.toLowerCase())
|
|
1649
|
+
)?.traceId.slice(0, 16) ?? "no match",
|
|
1650
|
+
"\u2026"
|
|
1651
|
+
] })
|
|
1652
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", justifyContent: "space-between", children: [
|
|
1653
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: drilldownTraceId == null ? "\u2191/\u2193 select \u2022 Enter open \u2022 Tab cycle tabs \u2022 Esc back \u2022 T trace \u2022 L logs \u2022 a AI \u2022 ? help" : "\u2191/\u2193 select \u2022 Tab cycle tabs \u2022 Esc back \u2022 T trace \u2022 L logs \u2022 a AI \u2022 ? help" }),
|
|
1654
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: viewMode === "trace" ? `traces ${filteredSummaries.length}/${traceSummaries.length}` : viewMode === "span" ? `spans ${filteredSpans.length}/${spans.length}` : viewMode === "service-summary" ? `services ${serviceStats.length}` : viewMode === "errors" ? `errors ${filteredErrorSummaries.length}/${errorSummaries.length}` : `logs ${filteredLogs.length}/${logs.length}` })
|
|
1655
|
+
] }),
|
|
1656
|
+
showHelp && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Views: t/l/v/E \u2022 Search: / \u2022 Filters: e/S/R/H/f/x \u2022 Capture: p/r/J \u2022 AI: a \u2022 Clear: c" })
|
|
969
1657
|
] }),
|
|
970
|
-
|
|
971
|
-
"filters:",
|
|
972
|
-
spanFilters.serviceName ? ` service=${spanFilters.serviceName}` : "",
|
|
973
|
-
spanFilters.route ? ` route=${spanFilters.route}` : "",
|
|
974
|
-
spanFilters.statusGroup && spanFilters.statusGroup !== "all" ? ` status=${spanFilters.statusGroup}` : ""
|
|
975
|
-
] }) }),
|
|
1658
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginBottom: 0, children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: spanFilters.serviceName || spanFilters.route || spanFilters.statusGroup !== "all" || spanFilters.traceId ? `filters:${spanFilters.serviceName ? ` service=${spanFilters.serviceName}` : ""}${spanFilters.route ? ` route=${spanFilters.route}` : ""}${spanFilters.statusGroup && spanFilters.statusGroup !== "all" ? ` status=${spanFilters.statusGroup}` : ""}${spanFilters.traceId ? ` trace=${spanFilters.traceId.slice(0, 8)}\u2026` : ""}` : "filters: none" }) }),
|
|
976
1659
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", gap: 2, children: [
|
|
977
1660
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
978
1661
|
ink.Box,
|
|
@@ -986,113 +1669,305 @@ ${json}
|
|
|
986
1669
|
children: [
|
|
987
1670
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 0, marginBottom: 1, children: [
|
|
988
1671
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: viewMode === "trace" ? "Recent traces" : viewMode === "span" ? "Recent spans" : viewMode === "service-summary" ? "Service summary" : viewMode === "errors" ? "Recent errors" : "Recent logs" }, "list-title"),
|
|
989
|
-
filterErrorsOnly && /* @__PURE__ */ jsxRuntime.
|
|
1672
|
+
filterErrorsOnly && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "red", children: [
|
|
1673
|
+
" ",
|
|
1674
|
+
"(errors only)"
|
|
1675
|
+
] }, "errors-only-label"),
|
|
990
1676
|
searchQuery && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
991
|
-
"
|
|
1677
|
+
" ",
|
|
1678
|
+
"/",
|
|
992
1679
|
searchQuery
|
|
993
1680
|
] }, "search-label")
|
|
994
1681
|
] }),
|
|
995
|
-
|
|
996
|
-
/* @__PURE__ */ jsxRuntime.
|
|
997
|
-
|
|
998
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: selectedTraceId == null ? filteredSummaries.slice(0, 20).map((t, i) => {
|
|
999
|
-
const isSel = i === selected;
|
|
1000
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
|
|
1001
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
|
|
1002
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: t.hasError ? "red" : void 0, children: truncate(t.rootName, 20) }),
|
|
1003
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1004
|
-
" ",
|
|
1005
|
-
formatDurationMs(t.durationMs)
|
|
1006
|
-
] }),
|
|
1007
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1008
|
-
" ",
|
|
1009
|
-
truncate(t.traceId, 8)
|
|
1010
|
-
] }),
|
|
1011
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1012
|
-
" ",
|
|
1013
|
-
formatRelative(t.lastEndTime)
|
|
1014
|
-
] })
|
|
1015
|
-
] }, t.traceId);
|
|
1016
|
-
}) : traceTree.slice(0, 20).map((node, i) => renderTreeRow(node, i)) }) : viewMode === "span" ? filteredSpans.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
|
|
1017
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No spans yet. Call a traced function or hit an endpoint to see them here." }),
|
|
1018
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Tip: trace() your handlers with autotel to get spans." })
|
|
1019
|
-
] }) : filteredSpans.slice(0, 20).map((s, i) => {
|
|
1020
|
-
const isSel = i === selected;
|
|
1021
|
-
const statusColor = s.status === "ERROR" ? "red" : s.durationMs > 500 ? "yellow" : "green";
|
|
1022
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
|
|
1023
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
|
|
1024
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: colors ? statusColor : void 0, children: truncate(s.name, 26) }),
|
|
1025
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1026
|
-
" ",
|
|
1027
|
-
formatDurationMs(s.durationMs)
|
|
1028
|
-
] }),
|
|
1029
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1030
|
-
" ",
|
|
1031
|
-
formatRelative(s.endTime)
|
|
1032
|
-
] })
|
|
1033
|
-
] }, `${s.spanId}-${s.startTime}`);
|
|
1034
|
-
}) : viewMode === "service-summary" ? serviceStats.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No service stats yet. Add `service.name` attributes to spans." }) }) : serviceStats.slice(0, 20).map((svc, i) => {
|
|
1035
|
-
const isSel = i === selected;
|
|
1036
|
-
const errorRate = svc.total ? svc.errors / svc.total * 100 : 0;
|
|
1037
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
|
|
1038
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
|
|
1039
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: truncate(svc.serviceName, 16) }),
|
|
1040
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1041
|
-
" ",
|
|
1042
|
-
svc.errors,
|
|
1043
|
-
"/",
|
|
1044
|
-
svc.total
|
|
1045
|
-
] }),
|
|
1682
|
+
drilldownTraceId != null && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1683
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 0, flexDirection: "row", gap: 2, children: [
|
|
1684
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "yellow", children: drilldownSummary?.rootName ?? "unknown" }),
|
|
1046
1685
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
"%"
|
|
1050
|
-
] }),
|
|
1051
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1052
|
-
" p95 ",
|
|
1053
|
-
formatDurationMs(svc.p95Ms)
|
|
1054
|
-
] })
|
|
1055
|
-
] }, svc.serviceName);
|
|
1056
|
-
}) : viewMode === "errors" ? errorSummaries.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No errors yet." }) }) : errorSummaries.slice(0, 20).map((e, i) => {
|
|
1057
|
-
const isSel = i === selected;
|
|
1058
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
|
|
1059
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
|
|
1060
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "red", children: truncate(e.rootName, 16) }),
|
|
1061
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1062
|
-
" ",
|
|
1063
|
-
truncate(e.serviceName, 10)
|
|
1064
|
-
] }),
|
|
1065
|
-
e.route && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1066
|
-
" ",
|
|
1067
|
-
truncate(e.route, 14)
|
|
1068
|
-
] }),
|
|
1069
|
-
typeof e.statusCode === "number" && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1070
|
-
" ",
|
|
1071
|
-
e.statusCode
|
|
1686
|
+
drilldownTraceId.slice(0, 16),
|
|
1687
|
+
"\u2026"
|
|
1072
1688
|
] }),
|
|
1689
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "green", children: drilldownSummary ? formatDurationMs(drilldownSummary.durationMs) : "?" }),
|
|
1073
1690
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1691
|
+
drilldownSpans.length,
|
|
1692
|
+
" spans \u2022 ",
|
|
1693
|
+
drilldownLogs.length,
|
|
1694
|
+
" logs"
|
|
1077
1695
|
] })
|
|
1078
|
-
] },
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1090
|
-
|
|
1091
|
-
|
|
1696
|
+
] }),
|
|
1697
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 0, flexDirection: "row", gap: 2, children: [
|
|
1698
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1699
|
+
ink.Text,
|
|
1700
|
+
{
|
|
1701
|
+
color: drilldownTab === "timeline" ? "yellow" : void 0,
|
|
1702
|
+
dimColor: drilldownTab !== "timeline",
|
|
1703
|
+
underline: drilldownTab === "timeline",
|
|
1704
|
+
children: "Timeline"
|
|
1705
|
+
}
|
|
1706
|
+
),
|
|
1707
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1708
|
+
ink.Text,
|
|
1709
|
+
{
|
|
1710
|
+
color: drilldownTab === "spans" ? "yellow" : void 0,
|
|
1711
|
+
dimColor: drilldownTab !== "spans",
|
|
1712
|
+
underline: drilldownTab === "spans",
|
|
1713
|
+
children: [
|
|
1714
|
+
"Spans (",
|
|
1715
|
+
drilldownSpans.length,
|
|
1716
|
+
")"
|
|
1717
|
+
]
|
|
1718
|
+
}
|
|
1719
|
+
),
|
|
1720
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1721
|
+
ink.Text,
|
|
1722
|
+
{
|
|
1723
|
+
color: drilldownTab === "logs" ? "yellow" : void 0,
|
|
1724
|
+
dimColor: drilldownTab !== "logs",
|
|
1725
|
+
underline: drilldownTab === "logs",
|
|
1726
|
+
children: [
|
|
1727
|
+
"Logs (",
|
|
1728
|
+
drilldownLogs.length,
|
|
1729
|
+
")"
|
|
1730
|
+
]
|
|
1731
|
+
}
|
|
1732
|
+
)
|
|
1733
|
+
] })
|
|
1734
|
+
] }),
|
|
1735
|
+
drilldownTraceId != null && drilldownTab === "timeline" && (() => {
|
|
1736
|
+
let traceStartMs = Infinity;
|
|
1737
|
+
for (const s of drilldownSummary?.spans ?? []) {
|
|
1738
|
+
if (s.startTime < traceStartMs) traceStartMs = s.startTime;
|
|
1739
|
+
}
|
|
1740
|
+
if (traceStartMs === Infinity) traceStartMs = 0;
|
|
1741
|
+
const traceDurMs = drilldownSummary?.durationMs ?? 1;
|
|
1742
|
+
const WATERFALL_WIDTH = 24;
|
|
1743
|
+
const items = drilldownTimeline.slice(drilldownScrollOffset, drilldownScrollOffset + LIST_HEIGHT);
|
|
1744
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1745
|
+
items.map((item, i) => {
|
|
1746
|
+
const isSel = i + drilldownScrollOffset === drilldownSelectedIndex;
|
|
1747
|
+
if (item.type === "span" && item.span) {
|
|
1748
|
+
const s = item.span;
|
|
1749
|
+
const node = drilldownTree.find(
|
|
1750
|
+
(n) => n.span.spanId === s.spanId
|
|
1751
|
+
);
|
|
1752
|
+
const depth = node?.depth ?? 0;
|
|
1753
|
+
const indent = " ".repeat(Math.min(depth, 4));
|
|
1754
|
+
const nameWidth = 24 - Math.min(depth, 4) * 2;
|
|
1755
|
+
const bar = buildWaterfallBar(
|
|
1756
|
+
s.startTime,
|
|
1757
|
+
s.durationMs,
|
|
1758
|
+
traceStartMs,
|
|
1759
|
+
traceDurMs,
|
|
1760
|
+
WATERFALL_WIDTH
|
|
1761
|
+
);
|
|
1762
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
|
|
1763
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { backgroundColor: isSel ? "gray" : void 0, color: isSel ? "white" : void 0, children: [
|
|
1764
|
+
isSel ? "\u25B8" : " ",
|
|
1765
|
+
indent,
|
|
1766
|
+
truncate(s.name, nameWidth)
|
|
1767
|
+
] }),
|
|
1768
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }),
|
|
1769
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: s.status === "ERROR" ? "red" : "green", children: bar }),
|
|
1770
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1771
|
+
" ",
|
|
1772
|
+
formatDurationMs(s.durationMs),
|
|
1773
|
+
s.kind ? ` ${s.kind}` : ""
|
|
1774
|
+
] })
|
|
1775
|
+
] }, `${s.spanId}-${i}`);
|
|
1776
|
+
} else if (item.type === "log" && item.log) {
|
|
1777
|
+
const l = item.log;
|
|
1778
|
+
const levelColor = l.level === "error" ? "red" : l.level === "warn" ? "yellow" : "blue";
|
|
1779
|
+
const relTime = drilldownSummary ? `+${formatDurationMs(l.time - traceStartMs)}` : "";
|
|
1780
|
+
const logOffset = drilldownSummary ? Math.floor((l.time - traceStartMs) / traceDurMs * WATERFALL_WIDTH) : 0;
|
|
1781
|
+
const clampedOffset = Math.max(0, Math.min(logOffset, WATERFALL_WIDTH - 1));
|
|
1782
|
+
const logBar = " ".repeat(clampedOffset) + "\xB7" + " ".repeat(WATERFALL_WIDTH - clampedOffset - 1);
|
|
1783
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
|
|
1784
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { backgroundColor: isSel ? "gray" : void 0, color: isSel ? "white" : void 0, children: [
|
|
1785
|
+
isSel ? "\u25B8" : " ",
|
|
1786
|
+
" ",
|
|
1787
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: levelColor, children: l.level.toUpperCase() }),
|
|
1788
|
+
" ",
|
|
1789
|
+
truncate(l.message, 18)
|
|
1790
|
+
] }),
|
|
1791
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }),
|
|
1792
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: logBar }),
|
|
1793
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1794
|
+
" ",
|
|
1795
|
+
relTime
|
|
1796
|
+
] })
|
|
1797
|
+
] }, `log-${i}`);
|
|
1798
|
+
}
|
|
1799
|
+
return null;
|
|
1800
|
+
}),
|
|
1801
|
+
Array.from({ length: Math.max(0, LIST_HEIGHT - items.length) }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }) }, `pad-${i}`))
|
|
1802
|
+
] });
|
|
1803
|
+
})(),
|
|
1804
|
+
drilldownTraceId != null && drilldownTab === "spans" && drilldownTree.slice(drilldownScrollOffset, drilldownScrollOffset + LIST_HEIGHT).map((node, i) => renderTreeRow(node, i + drilldownScrollOffset)),
|
|
1805
|
+
drilldownTraceId != null && drilldownTab === "spans" && Array.from({ length: Math.max(0, LIST_HEIGHT - Math.min(drilldownTree.length, LIST_HEIGHT)) }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }) }, `pad-${i}`)),
|
|
1806
|
+
drilldownTraceId != null && drilldownTab === "logs" && drilldownLogs.slice(drilldownScrollOffset, drilldownScrollOffset + LIST_HEIGHT).map((log, i) => {
|
|
1807
|
+
const isSel = i + drilldownScrollOffset === drilldownSelectedIndex;
|
|
1808
|
+
const levelColor = log.level === "error" ? "red" : log.level === "warn" ? "yellow" : log.level === "info" ? "green" : void 0;
|
|
1809
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1810
|
+
ink.Text,
|
|
1811
|
+
{
|
|
1812
|
+
backgroundColor: isSel ? "gray" : void 0,
|
|
1813
|
+
color: isSel ? "white" : void 0,
|
|
1814
|
+
children: [
|
|
1815
|
+
isSel ? "\u25B8" : " ",
|
|
1816
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: levelColor, children: [
|
|
1817
|
+
" ",
|
|
1818
|
+
log.level.toUpperCase()
|
|
1819
|
+
] }),
|
|
1820
|
+
" ",
|
|
1821
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1822
|
+
"[",
|
|
1823
|
+
truncate(log.message, 50),
|
|
1824
|
+
"]"
|
|
1825
|
+
] })
|
|
1826
|
+
]
|
|
1827
|
+
}
|
|
1828
|
+
) }, `log-${i}`);
|
|
1829
|
+
}),
|
|
1830
|
+
drilldownTraceId != null && drilldownTab === "logs" && Array.from({ length: Math.max(0, LIST_HEIGHT - Math.min(drilldownLogs.length, LIST_HEIGHT)) }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }) }, `pad-${i}`)),
|
|
1831
|
+
drilldownTraceId == null && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: viewMode === "trace" ? filteredSummaries.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1832
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
|
|
1833
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No traces yet. Call a traced function or hit an endpoint to see them here." }),
|
|
1834
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Tip: trace() your handlers with autotel to get spans." })
|
|
1835
|
+
] }),
|
|
1836
|
+
Array.from({ length: Math.max(0, LIST_HEIGHT - 2) }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }) }, `pad-${i}`))
|
|
1837
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1838
|
+
filteredSummaries.slice(0, 20).map((t2, i) => {
|
|
1839
|
+
const isSel = i === selected;
|
|
1840
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
|
|
1841
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u25B8 " : " " }),
|
|
1842
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: t2.hasError ? "red" : "yellow", bold: isSel, children: truncate(t2.rootName, 28) }),
|
|
1843
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1844
|
+
" ",
|
|
1845
|
+
t2.spans.length,
|
|
1846
|
+
" spans"
|
|
1847
|
+
] }),
|
|
1848
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "green", children: [
|
|
1849
|
+
" ",
|
|
1850
|
+
formatDurationMs(t2.durationMs)
|
|
1851
|
+
] }),
|
|
1852
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1853
|
+
" ",
|
|
1854
|
+
formatRelative(t2.lastEndTime)
|
|
1855
|
+
] }),
|
|
1856
|
+
t2.hasError && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "red", children: " \u25CF" })
|
|
1857
|
+
] }, t2.traceId);
|
|
1858
|
+
}),
|
|
1859
|
+
Array.from({ length: Math.max(0, LIST_HEIGHT - Math.min(filteredSummaries.length, 20)) }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }) }, `pad-${i}`))
|
|
1860
|
+
] }) : viewMode === "span" ? filteredSpans.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1861
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
|
|
1862
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No spans yet. Call a traced function or hit an endpoint to see them here." }),
|
|
1863
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Tip: trace() your handlers with autotel to get spans." })
|
|
1864
|
+
] }),
|
|
1865
|
+
Array.from({ length: Math.max(0, LIST_HEIGHT - 2) }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }) }, `pad-${i}`))
|
|
1866
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1867
|
+
filteredSpans.slice(0, 20).map((s, i) => {
|
|
1868
|
+
const isSel = i === selected;
|
|
1869
|
+
const statusColor = s.status === "ERROR" ? "red" : s.durationMs > 500 ? "yellow" : "green";
|
|
1870
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1871
|
+
ink.Box,
|
|
1872
|
+
{
|
|
1873
|
+
flexDirection: "row",
|
|
1874
|
+
children: [
|
|
1875
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
|
|
1876
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: colors ? statusColor : void 0, children: truncate(s.name, 26) }),
|
|
1877
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1878
|
+
" ",
|
|
1879
|
+
formatDurationMs(s.durationMs)
|
|
1880
|
+
] }),
|
|
1881
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1882
|
+
" ",
|
|
1883
|
+
formatRelative(s.endTime)
|
|
1884
|
+
] })
|
|
1885
|
+
]
|
|
1886
|
+
},
|
|
1887
|
+
`${s.spanId}-${s.startTime}`
|
|
1888
|
+
);
|
|
1889
|
+
}),
|
|
1890
|
+
Array.from({ length: Math.max(0, LIST_HEIGHT - Math.min(filteredSpans.length, 20)) }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }) }, `pad-${i}`))
|
|
1891
|
+
] }) : viewMode === "service-summary" ? serviceStats.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1892
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No service stats yet. Add `service.name` attributes to spans." }) }),
|
|
1893
|
+
Array.from({ length: Math.max(0, LIST_HEIGHT - 1) }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }) }, `pad-${i}`))
|
|
1894
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1895
|
+
serviceStats.slice(0, 20).map((svc, i) => {
|
|
1896
|
+
const isSel = i === selected;
|
|
1897
|
+
const errorRate = svc.total ? svc.errors / svc.total * 100 : 0;
|
|
1898
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
|
|
1899
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
|
|
1900
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: truncate(svc.serviceName, 16) }),
|
|
1901
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1902
|
+
" ",
|
|
1903
|
+
svc.errors,
|
|
1904
|
+
"/",
|
|
1905
|
+
svc.total
|
|
1906
|
+
] }),
|
|
1907
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1908
|
+
" ",
|
|
1909
|
+
errorRate.toFixed(0),
|
|
1910
|
+
"%"
|
|
1911
|
+
] }),
|
|
1912
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1913
|
+
" p95 ",
|
|
1914
|
+
formatDurationMs(svc.p95Ms)
|
|
1915
|
+
] })
|
|
1916
|
+
] }, svc.serviceName);
|
|
1917
|
+
}),
|
|
1918
|
+
Array.from({ length: Math.max(0, LIST_HEIGHT - Math.min(serviceStats.length, 20)) }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }) }, `pad-${i}`))
|
|
1919
|
+
] }) : viewMode === "errors" ? filteredErrorSummaries.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1920
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No errors yet." }) }),
|
|
1921
|
+
Array.from({ length: Math.max(0, LIST_HEIGHT - 1) }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }) }, `pad-${i}`))
|
|
1922
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1923
|
+
filteredErrorSummaries.slice(0, 20).map((e, i) => {
|
|
1924
|
+
const isSel = i === selected;
|
|
1925
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
|
|
1926
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
|
|
1927
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "red", children: truncate(e.rootName, 16) }),
|
|
1928
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1929
|
+
" ",
|
|
1930
|
+
truncate(e.serviceName, 10)
|
|
1931
|
+
] }),
|
|
1932
|
+
e.route && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1933
|
+
" ",
|
|
1934
|
+
truncate(e.route, 14)
|
|
1935
|
+
] }),
|
|
1936
|
+
typeof e.statusCode === "number" && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1937
|
+
" ",
|
|
1938
|
+
e.statusCode
|
|
1939
|
+
] }),
|
|
1940
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1941
|
+
" (",
|
|
1942
|
+
e.errorCount,
|
|
1943
|
+
")"
|
|
1944
|
+
] })
|
|
1945
|
+
] }, e.traceId);
|
|
1946
|
+
}),
|
|
1947
|
+
Array.from({ length: Math.max(0, LIST_HEIGHT - Math.min(filteredErrorSummaries.length, 20)) }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }) }, `pad-${i}`))
|
|
1948
|
+
] }) : filteredLogs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1949
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
|
|
1950
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No logs yet. Emit request logs or canonical log lines to see them here." }),
|
|
1951
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Tip: hook getTerminalLogStream() into your canonical log line drain." })
|
|
1952
|
+
] }),
|
|
1953
|
+
Array.from({ length: Math.max(0, LIST_HEIGHT - 2) }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }) }, `pad-${i}`))
|
|
1954
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1955
|
+
filteredLogs.slice(0, 20).map((log, i) => {
|
|
1956
|
+
const isSel = i === selected;
|
|
1957
|
+
const levelColor = log.level === "error" ? "red" : log.level === "warn" ? "yellow" : log.level === "debug" ? "gray" : "green";
|
|
1958
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
|
|
1959
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
|
|
1960
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: colors ? levelColor : void 0, children: truncate(log.level.toUpperCase(), 5) }),
|
|
1961
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }),
|
|
1962
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: truncate(log.message, 32) })
|
|
1963
|
+
] }, `${log.time}-${i}`);
|
|
1964
|
+
}),
|
|
1965
|
+
Array.from({ length: Math.max(0, LIST_HEIGHT - Math.min(filteredLogs.length, 20)) }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }) }, `pad-${i}`))
|
|
1966
|
+
] }) })
|
|
1092
1967
|
]
|
|
1093
1968
|
}
|
|
1094
1969
|
),
|
|
1095
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1970
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1096
1971
|
ink.Box,
|
|
1097
1972
|
{
|
|
1098
1973
|
flexDirection: "column",
|
|
@@ -1101,11 +1976,139 @@ ${json}
|
|
|
1101
1976
|
borderColor: "gray",
|
|
1102
1977
|
paddingX: 1,
|
|
1103
1978
|
paddingY: 0,
|
|
1104
|
-
children: [
|
|
1979
|
+
children: aiActive ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1980
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 1, justifyContent: "space-between", children: [
|
|
1981
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "AI Assistant" }),
|
|
1982
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: aiState.status === "streaming" ? "(streaming...)" : aiState.status === "unconfigured" ? "(no provider)" : aiState.status === "error" ? "(error)" : "" })
|
|
1983
|
+
] }),
|
|
1984
|
+
aiState.status === "unconfigured" ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
|
|
1985
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No AI provider configured." }),
|
|
1986
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Set AI_PROVIDER and AI_MODEL env vars, or start Ollama locally." }),
|
|
1987
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Press 'a' to close this panel." })
|
|
1988
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1989
|
+
aiMessages.length === 0 && aiState.status !== "error" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Ask a question about your telemetry data. Press Enter to send." }),
|
|
1990
|
+
aiMessages.slice(-10).map((msg, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1991
|
+
ink.Box,
|
|
1992
|
+
{
|
|
1993
|
+
flexDirection: "column",
|
|
1994
|
+
marginBottom: msg.role === "assistant" ? 1 : 0,
|
|
1995
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: msg.role === "user" ? "cyan" : void 0, children: [
|
|
1996
|
+
msg.role === "user" ? "> " : "",
|
|
1997
|
+
msg.content.slice(0, 500),
|
|
1998
|
+
msg.content.length > 500 ? "..." : ""
|
|
1999
|
+
] })
|
|
2000
|
+
},
|
|
2001
|
+
i
|
|
2002
|
+
)),
|
|
2003
|
+
aiState.status === "error" && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "red", children: [
|
|
2004
|
+
"Error: ",
|
|
2005
|
+
aiState.message
|
|
2006
|
+
] }),
|
|
2007
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2008
|
+
ink.Box,
|
|
2009
|
+
{
|
|
2010
|
+
marginTop: 1,
|
|
2011
|
+
borderStyle: "single",
|
|
2012
|
+
borderColor: "cyan",
|
|
2013
|
+
paddingX: 1,
|
|
2014
|
+
children: [
|
|
2015
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "cyan", children: "> " }),
|
|
2016
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: aiInput || (aiInputMode ? "(type your question)" : "(press a to focus)") })
|
|
2017
|
+
]
|
|
2018
|
+
}
|
|
2019
|
+
)
|
|
2020
|
+
] })
|
|
2021
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1105
2022
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginBottom: 1, children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Details" }) }),
|
|
1106
|
-
|
|
1107
|
-
const
|
|
1108
|
-
|
|
2023
|
+
drilldownTraceId != null && drilldownSelectedItem?.type === "span" && drilldownSelectedItem.span ? (() => {
|
|
2024
|
+
const span = drilldownSelectedItem.span;
|
|
2025
|
+
const { key: keyAttrs, rest: restAttrs } = keyAttrsAndRest(
|
|
2026
|
+
span.attributes
|
|
2027
|
+
);
|
|
2028
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2029
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
2030
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Name: " }),
|
|
2031
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: span.name })
|
|
2032
|
+
] }),
|
|
2033
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
2034
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Status: " }),
|
|
2035
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: span.status === "ERROR" ? "red" : "green", children: span.status })
|
|
2036
|
+
] }),
|
|
2037
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
2038
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Duration: " }),
|
|
2039
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: formatDurationMs(span.durationMs) })
|
|
2040
|
+
] }),
|
|
2041
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2042
|
+
"Trace: ",
|
|
2043
|
+
span.traceId
|
|
2044
|
+
] }),
|
|
2045
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2046
|
+
"Span: ",
|
|
2047
|
+
span.spanId
|
|
2048
|
+
] }),
|
|
2049
|
+
span.parentSpanId && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2050
|
+
"Parent: ",
|
|
2051
|
+
span.parentSpanId
|
|
2052
|
+
] }),
|
|
2053
|
+
span.kind && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2054
|
+
"Kind: ",
|
|
2055
|
+
span.kind
|
|
2056
|
+
] }),
|
|
2057
|
+
keyAttrs.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 1, flexDirection: "column", children: [
|
|
2058
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Key attributes" }),
|
|
2059
|
+
keyAttrs.slice(0, 6).map(([k, v]) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2060
|
+
truncate(k, 18),
|
|
2061
|
+
": ",
|
|
2062
|
+
truncate(String(v), 28)
|
|
2063
|
+
] }, k))
|
|
2064
|
+
] }),
|
|
2065
|
+
restAttrs.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 1, flexDirection: "column", children: [
|
|
2066
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Attributes" }),
|
|
2067
|
+
restAttrs.slice(0, 8).map(([k, v]) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2068
|
+
truncate(k, 18),
|
|
2069
|
+
": ",
|
|
2070
|
+
truncate(String(v), 28)
|
|
2071
|
+
] }, k))
|
|
2072
|
+
] }),
|
|
2073
|
+
keyAttrs.length === 0 && restAttrs.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "(no attributes)" })
|
|
2074
|
+
] });
|
|
2075
|
+
})() : drilldownTraceId != null && drilldownSelectedItem?.type === "log" && drilldownSelectedItem.log ? (() => {
|
|
2076
|
+
const log = drilldownSelectedItem.log;
|
|
2077
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2078
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
2079
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Level: " }),
|
|
2080
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: log.level.toUpperCase() })
|
|
2081
|
+
] }),
|
|
2082
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
2083
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Time: " }),
|
|
2084
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: new Date(log.time).toISOString() })
|
|
2085
|
+
] }),
|
|
2086
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
2087
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Message: " }),
|
|
2088
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: log.message })
|
|
2089
|
+
] }),
|
|
2090
|
+
log.traceId && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2091
|
+
"Trace: ",
|
|
2092
|
+
log.traceId
|
|
2093
|
+
] }),
|
|
2094
|
+
log.spanId && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2095
|
+
"Span: ",
|
|
2096
|
+
log.spanId
|
|
2097
|
+
] }),
|
|
2098
|
+
log.attributes && Object.keys(log.attributes).length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 1, flexDirection: "column", children: [
|
|
2099
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Attributes" }),
|
|
2100
|
+
Object.entries(log.attributes).slice(0, 10).map(([k, v]) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2101
|
+
truncate(k, 18),
|
|
2102
|
+
": ",
|
|
2103
|
+
truncate(String(v), 40)
|
|
2104
|
+
] }, k))
|
|
2105
|
+
] })
|
|
2106
|
+
] });
|
|
2107
|
+
})() : drilldownTraceId == null ? null : /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Select an item to view details." }),
|
|
2108
|
+
drilldownTraceId == null && (viewMode === "errors" ? (() => {
|
|
2109
|
+
const e = filteredErrorSummaries[selected] ?? null;
|
|
2110
|
+
if (!e)
|
|
2111
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Select an error to view details." });
|
|
1109
2112
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1110
2113
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
1111
2114
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Trace: " }),
|
|
@@ -1121,11 +2124,12 @@ ${json}
|
|
|
1121
2124
|
"Errors: ",
|
|
1122
2125
|
e.errorCount
|
|
1123
2126
|
] }),
|
|
1124
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "
|
|
2127
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Press T to jump to trace view for this trace." })
|
|
1125
2128
|
] });
|
|
1126
2129
|
})() : viewMode === "service-summary" ? (() => {
|
|
1127
2130
|
const svc = serviceStats[selected] ?? null;
|
|
1128
|
-
if (!svc)
|
|
2131
|
+
if (!svc)
|
|
2132
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Select a service to view details." });
|
|
1129
2133
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1130
2134
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
1131
2135
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Service: " }),
|
|
@@ -1139,7 +2143,8 @@ ${json}
|
|
|
1139
2143
|
" | Avg:",
|
|
1140
2144
|
" ",
|
|
1141
2145
|
formatDurationMs(svc.avgMs),
|
|
1142
|
-
" | P95:
|
|
2146
|
+
" | P95:",
|
|
2147
|
+
" ",
|
|
1143
2148
|
formatDurationMs(svc.p95Ms)
|
|
1144
2149
|
] }),
|
|
1145
2150
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 1, flexDirection: "column", children: [
|
|
@@ -1159,7 +2164,8 @@ ${json}
|
|
|
1159
2164
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Hot spans" }),
|
|
1160
2165
|
selectedServiceHotSpans.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "(no spans)" }) : selectedServiceHotSpans.map((h) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1161
2166
|
truncate(h.name, 20),
|
|
1162
|
-
" p95
|
|
2167
|
+
" p95",
|
|
2168
|
+
" ",
|
|
1163
2169
|
formatDurationMs(h.p95Ms),
|
|
1164
2170
|
" (",
|
|
1165
2171
|
h.count,
|
|
@@ -1209,7 +2215,8 @@ ${json}
|
|
|
1209
2215
|
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1210
2216
|
"+",
|
|
1211
2217
|
relMs,
|
|
1212
|
-
"ms span
|
|
2218
|
+
"ms span",
|
|
2219
|
+
" ",
|
|
1213
2220
|
truncate(item.span.name, 20)
|
|
1214
2221
|
] }, `span-${idx}`);
|
|
1215
2222
|
}
|
|
@@ -1217,7 +2224,8 @@ ${json}
|
|
|
1217
2224
|
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
1218
2225
|
"+",
|
|
1219
2226
|
relMs,
|
|
1220
|
-
"ms log
|
|
2227
|
+
"ms log",
|
|
2228
|
+
" ",
|
|
1221
2229
|
truncate(item.log.message, 24)
|
|
1222
2230
|
] }, `log-${idx}`);
|
|
1223
2231
|
}
|
|
@@ -1232,16 +2240,25 @@ ${json}
|
|
|
1232
2240
|
] }),
|
|
1233
2241
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
1234
2242
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Status: " }),
|
|
1235
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2243
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2244
|
+
ink.Text,
|
|
2245
|
+
{
|
|
2246
|
+
color: currentSpan.status === "ERROR" ? "red" : "green",
|
|
2247
|
+
children: currentSpan.status
|
|
2248
|
+
}
|
|
2249
|
+
)
|
|
1236
2250
|
] }),
|
|
1237
2251
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
1238
2252
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Duration: " }),
|
|
1239
2253
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
1240
2254
|
formatDurationMs(currentSpan.durationMs),
|
|
1241
2255
|
perSpanNameStats.byName.has(currentSpan.name) && (() => {
|
|
1242
|
-
const p = perSpanNameStats.byName.get(
|
|
2256
|
+
const p = perSpanNameStats.byName.get(
|
|
2257
|
+
currentSpan.name
|
|
2258
|
+
);
|
|
1243
2259
|
const ratio = p.avgMs > 0 ? currentSpan.durationMs / p.avgMs : 1;
|
|
1244
|
-
if (ratio >= 1.5)
|
|
2260
|
+
if (ratio >= 1.5)
|
|
2261
|
+
return ` (${ratio.toFixed(1)}x avg)`;
|
|
1245
2262
|
return "";
|
|
1246
2263
|
})()
|
|
1247
2264
|
] })
|
|
@@ -1284,10 +2301,12 @@ ${json}
|
|
|
1284
2301
|
keyAttrs.length === 0 && restAttrs.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "(no attributes)" })
|
|
1285
2302
|
] });
|
|
1286
2303
|
})(),
|
|
1287
|
-
waterfallSpans.length > 0 &&
|
|
2304
|
+
waterfallSpans.length > 0 && drilldownTraceId != null && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 1, flexDirection: "column", children: [
|
|
1288
2305
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Waterfall" }),
|
|
1289
2306
|
waterfallSpans.slice(0, 10).map((w) => {
|
|
1290
|
-
const barLen = Math.round(
|
|
2307
|
+
const barLen = Math.round(
|
|
2308
|
+
w.span.durationMs / waterfallMaxMs * barWidth
|
|
2309
|
+
) || 1;
|
|
1291
2310
|
const bar = "\u2588".repeat(barLen);
|
|
1292
2311
|
const indent = " ".repeat(w.depth);
|
|
1293
2312
|
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
|
|
@@ -1304,8 +2323,8 @@ ${json}
|
|
|
1304
2323
|
] }, w.span.spanId);
|
|
1305
2324
|
})
|
|
1306
2325
|
] })
|
|
1307
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Select a trace or span to view details." })
|
|
1308
|
-
]
|
|
2326
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Select a trace or span to view details." }))
|
|
2327
|
+
] })
|
|
1309
2328
|
}
|
|
1310
2329
|
)
|
|
1311
2330
|
] }),
|
|
@@ -1314,11 +2333,13 @@ ${json}
|
|
|
1314
2333
|
stats.total,
|
|
1315
2334
|
" | Span errors: ",
|
|
1316
2335
|
stats.errors,
|
|
1317
|
-
" | Logs:
|
|
2336
|
+
" | Logs:",
|
|
2337
|
+
" ",
|
|
1318
2338
|
logStats.total,
|
|
1319
2339
|
" | Log errors: ",
|
|
1320
2340
|
logStats.errors,
|
|
1321
|
-
" | Avg:
|
|
2341
|
+
" | Avg:",
|
|
2342
|
+
" ",
|
|
1322
2343
|
formatDurationMs(stats.avg),
|
|
1323
2344
|
" | P95: ",
|
|
1324
2345
|
formatDurationMs(stats.p95)
|
|
@@ -1336,6 +2357,7 @@ function renderTerminal(options = {}, stream) {
|
|
|
1336
2357
|
const showStats = options.showStats !== false;
|
|
1337
2358
|
const maxSpans = options.maxSpans ?? 100;
|
|
1338
2359
|
const colors = options.colors ?? Boolean(process.stdout.isTTY);
|
|
2360
|
+
const aiConfig = options.ai;
|
|
1339
2361
|
const stdinOption = process.stdin.isTTY ? process.stdin : void 0;
|
|
1340
2362
|
if (stream) {
|
|
1341
2363
|
try {
|
|
@@ -1348,7 +2370,8 @@ function renderTerminal(options = {}, stream) {
|
|
|
1348
2370
|
maxSpans,
|
|
1349
2371
|
colors,
|
|
1350
2372
|
stream,
|
|
1351
|
-
logStream: getTerminalLogStream()
|
|
2373
|
+
logStream: getTerminalLogStream(),
|
|
2374
|
+
aiConfig
|
|
1352
2375
|
}
|
|
1353
2376
|
),
|
|
1354
2377
|
{ stdin: stdinOption }
|
|
@@ -1464,7 +2487,8 @@ function toMs(unixNano) {
|
|
|
1464
2487
|
}
|
|
1465
2488
|
function mapStatus2(code) {
|
|
1466
2489
|
const normalized = typeof code === "string" ? code.toUpperCase() : code;
|
|
1467
|
-
if (normalized === 1 || normalized === "STATUS_CODE_OK" || normalized === "OK")
|
|
2490
|
+
if (normalized === 1 || normalized === "STATUS_CODE_OK" || normalized === "OK")
|
|
2491
|
+
return "OK";
|
|
1468
2492
|
if (normalized === 2 || normalized === "STATUS_CODE_ERROR" || normalized === "ERROR") {
|
|
1469
2493
|
return "ERROR";
|
|
1470
2494
|
}
|
|
@@ -1635,33 +2659,39 @@ function countOtlpMetrics(payload) {
|
|
|
1635
2659
|
|
|
1636
2660
|
// src/cli.ts
|
|
1637
2661
|
function printHelp() {
|
|
1638
|
-
process.stdout.write(
|
|
2662
|
+
process.stdout.write(
|
|
2663
|
+
String.raw`autotel-terminal - Standalone OTLP receiver with terminal dashboard
|
|
1639
2664
|
|
|
1640
2665
|
Usage: autotel-terminal [options]
|
|
1641
2666
|
|
|
1642
2667
|
Options:
|
|
1643
|
-
-p, --port <port>
|
|
1644
|
-
-H, --host <host>
|
|
1645
|
-
-t, --title <title>
|
|
1646
|
-
-h, --help
|
|
1647
|
-
-v, --version
|
|
2668
|
+
-p, --port <port> Port to listen on (default: 4319, env: AUTOTEL_TERMINAL_PORT)
|
|
2669
|
+
-H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_TERMINAL_HOST)
|
|
2670
|
+
-t, --title <title> Dashboard title (env: AUTOTEL_TERMINAL_TITLE)
|
|
2671
|
+
-h, --help Show this help message
|
|
2672
|
+
-v, --version Show version number
|
|
2673
|
+
|
|
2674
|
+
AI Options:
|
|
2675
|
+
--ai-provider <provider> AI provider: ollama, openai, openai-compatible (env: AI_PROVIDER)
|
|
2676
|
+
--ai-model <model> AI model name (env: AI_MODEL)
|
|
2677
|
+
--ai-api-key <key> API key for cloud providers (env: AI_API_KEY)
|
|
2678
|
+
--ai-base-url <url> Custom AI endpoint URL (env: AI_BASE_URL)
|
|
2679
|
+
|
|
2680
|
+
Auto-detection: if Ollama is running locally, it is used automatically.
|
|
2681
|
+
If OPENAI_API_KEY is set, OpenAI is used. Press 'a' in the dashboard to toggle AI.
|
|
1648
2682
|
|
|
1649
2683
|
Endpoints:
|
|
1650
|
-
POST /v1/traces
|
|
1651
|
-
POST /v1/logs
|
|
1652
|
-
POST /v1/metrics
|
|
1653
|
-
GET /healthz
|
|
2684
|
+
POST /v1/traces Receive OTLP JSON trace data
|
|
2685
|
+
POST /v1/logs Receive OTLP JSON log data
|
|
2686
|
+
POST /v1/metrics Receive OTLP JSON metric data (accepted and counted)
|
|
2687
|
+
GET /healthz Health check
|
|
1654
2688
|
|
|
1655
2689
|
Examples:
|
|
1656
2690
|
npx autotel-terminal
|
|
1657
|
-
npx autotel-terminal --
|
|
1658
|
-
npx autotel-terminal -
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
OTEL_EXPORTER_OTLP_PROTOCOL=http/json \
|
|
1662
|
-
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4319 \
|
|
1663
|
-
node app.js
|
|
1664
|
-
` + "\n");
|
|
2691
|
+
npx autotel-terminal --ai-provider ollama --ai-model granite4
|
|
2692
|
+
AI_API_KEY=sk-... npx autotel-terminal --ai-provider openai --ai-model gpt-4o
|
|
2693
|
+
` + "\n"
|
|
2694
|
+
);
|
|
1665
2695
|
}
|
|
1666
2696
|
function printVersion() {
|
|
1667
2697
|
try {
|
|
@@ -1678,7 +2708,8 @@ function parseArgs(argv) {
|
|
|
1678
2708
|
const options = {
|
|
1679
2709
|
port: Number(process.env.AUTOTEL_TERMINAL_PORT || 4319),
|
|
1680
2710
|
host: process.env.AUTOTEL_TERMINAL_HOST || "127.0.0.1",
|
|
1681
|
-
title: process.env.AUTOTEL_TERMINAL_TITLE
|
|
2711
|
+
title: process.env.AUTOTEL_TERMINAL_TITLE,
|
|
2712
|
+
ai: {}
|
|
1682
2713
|
};
|
|
1683
2714
|
for (let i = 0; i < argv.length; i++) {
|
|
1684
2715
|
const arg = argv[i];
|
|
@@ -1704,6 +2735,26 @@ function parseArgs(argv) {
|
|
|
1704
2735
|
if ((arg === "--title" || arg === "-t") && next) {
|
|
1705
2736
|
options.title = next;
|
|
1706
2737
|
i++;
|
|
2738
|
+
continue;
|
|
2739
|
+
}
|
|
2740
|
+
if (arg === "--ai-provider" && next) {
|
|
2741
|
+
options.ai.provider = next;
|
|
2742
|
+
i++;
|
|
2743
|
+
continue;
|
|
2744
|
+
}
|
|
2745
|
+
if (arg === "--ai-model" && next) {
|
|
2746
|
+
options.ai.model = next;
|
|
2747
|
+
i++;
|
|
2748
|
+
continue;
|
|
2749
|
+
}
|
|
2750
|
+
if (arg === "--ai-api-key" && next) {
|
|
2751
|
+
options.ai.apiKey = next;
|
|
2752
|
+
i++;
|
|
2753
|
+
continue;
|
|
2754
|
+
}
|
|
2755
|
+
if (arg === "--ai-base-url" && next) {
|
|
2756
|
+
options.ai.baseUrl = next;
|
|
2757
|
+
i++;
|
|
1707
2758
|
}
|
|
1708
2759
|
}
|
|
1709
2760
|
return options;
|
|
@@ -1718,7 +2769,8 @@ async function main() {
|
|
|
1718
2769
|
const logStream = getTerminalLogStream();
|
|
1719
2770
|
renderTerminal(
|
|
1720
2771
|
{
|
|
1721
|
-
title: options.title || `Autotel Terminal (${options.host}:${options.port})
|
|
2772
|
+
title: options.title || `Autotel Terminal (${options.host}:${options.port})`,
|
|
2773
|
+
ai: options.ai
|
|
1722
2774
|
},
|
|
1723
2775
|
spanStream
|
|
1724
2776
|
);
|