autotel-terminal 17.0.1 → 17.0.2
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 +1179 -170
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1179 -170
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1129 -149
- 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 +1129 -149
- 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 +1362 -425
- 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/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;
|
|
@@ -423,10 +428,11 @@ function getStatusCode(span) {
|
|
|
423
428
|
return void 0;
|
|
424
429
|
}
|
|
425
430
|
function applySpanFilters(spans, state) {
|
|
426
|
-
const { serviceName, route, statusGroup, errorsOnly, searchQuery } = state;
|
|
431
|
+
const { serviceName, route, statusGroup, errorsOnly, searchQuery, traceId } = state;
|
|
427
432
|
const q = searchQuery?.trim().toLowerCase() ?? "";
|
|
428
433
|
return spans.filter((span) => {
|
|
429
434
|
const attrs = span.attributes ?? {};
|
|
435
|
+
if (traceId && span.traceId !== traceId) return false;
|
|
430
436
|
if (errorsOnly && span.status !== "ERROR") return false;
|
|
431
437
|
if (serviceName) {
|
|
432
438
|
const svc = getServiceName(span);
|
|
@@ -482,17 +488,17 @@ function getStatusCodeFromSpans(spans) {
|
|
|
482
488
|
}
|
|
483
489
|
function buildErrorSummaries(traceSummaries) {
|
|
484
490
|
const out = [];
|
|
485
|
-
for (const
|
|
486
|
-
const errorSpans =
|
|
491
|
+
for (const t2 of traceSummaries) {
|
|
492
|
+
const errorSpans = t2.spans.filter((s) => s.status === "ERROR");
|
|
487
493
|
if (errorSpans.length === 0) continue;
|
|
488
494
|
out.push({
|
|
489
|
-
traceId:
|
|
490
|
-
rootName:
|
|
491
|
-
serviceName: getServiceNameFromSpans(
|
|
492
|
-
route: getRouteFromSpans(
|
|
493
|
-
statusCode: getStatusCodeFromSpans(
|
|
495
|
+
traceId: t2.traceId,
|
|
496
|
+
rootName: t2.rootName,
|
|
497
|
+
serviceName: getServiceNameFromSpans(t2.spans),
|
|
498
|
+
route: getRouteFromSpans(t2.spans),
|
|
499
|
+
statusCode: getStatusCodeFromSpans(t2.spans),
|
|
494
500
|
errorCount: errorSpans.length,
|
|
495
|
-
lastEndTime:
|
|
501
|
+
lastEndTime: t2.lastEndTime
|
|
496
502
|
});
|
|
497
503
|
}
|
|
498
504
|
out.sort((a, b) => b.lastEndTime - a.lastEndTime);
|
|
@@ -528,6 +534,302 @@ function exportTraceToJson(trace, logs) {
|
|
|
528
534
|
};
|
|
529
535
|
return JSON.stringify(exported, null, 2);
|
|
530
536
|
}
|
|
537
|
+
|
|
538
|
+
// src/ai/provider.ts
|
|
539
|
+
async function detectOllama(baseUrl = "http://127.0.0.1:11434") {
|
|
540
|
+
try {
|
|
541
|
+
const res = await fetch(`${baseUrl}/api/tags`, {
|
|
542
|
+
signal: AbortSignal.timeout(1e3)
|
|
543
|
+
});
|
|
544
|
+
return res.ok;
|
|
545
|
+
} catch {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function resolveConfig(options = {}) {
|
|
550
|
+
const provider = options.provider ?? process.env.AI_PROVIDER ?? void 0;
|
|
551
|
+
const model = options.model ?? process.env.AI_MODEL;
|
|
552
|
+
const apiKey = options.apiKey ?? process.env.AI_API_KEY ?? (provider === "openai" ? process.env.OPENAI_API_KEY : void 0);
|
|
553
|
+
const baseUrl = options.baseUrl ?? process.env.AI_BASE_URL;
|
|
554
|
+
if (provider) {
|
|
555
|
+
return {
|
|
556
|
+
provider,
|
|
557
|
+
model: model ?? (provider === "ollama" ? "granite4" : "gpt-4o"),
|
|
558
|
+
apiKey,
|
|
559
|
+
baseUrl
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
var defaultAutoDetectDeps = { detectOllama };
|
|
565
|
+
async function resolveConfigWithAutoDetect(options = {}, deps = defaultAutoDetectDeps) {
|
|
566
|
+
const config = resolveConfig(options);
|
|
567
|
+
if (config) return config;
|
|
568
|
+
const ollamaUrl = options.baseUrl ?? process.env.AI_BASE_URL ?? "http://127.0.0.1:11434";
|
|
569
|
+
if (await deps.detectOllama(ollamaUrl)) {
|
|
570
|
+
return {
|
|
571
|
+
provider: "ollama",
|
|
572
|
+
model: options.model ?? process.env.AI_MODEL ?? "granite4",
|
|
573
|
+
baseUrl: ollamaUrl
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
const openaiKey = options.apiKey ?? process.env.AI_API_KEY ?? process.env.OPENAI_API_KEY;
|
|
577
|
+
if (openaiKey) {
|
|
578
|
+
return {
|
|
579
|
+
provider: "openai",
|
|
580
|
+
model: options.model ?? process.env.AI_MODEL ?? "gpt-4o",
|
|
581
|
+
apiKey: openaiKey
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
async function createAIModel(config) {
|
|
587
|
+
switch (config.provider) {
|
|
588
|
+
case "ollama": {
|
|
589
|
+
const { createOllama } = await import('ai-sdk-ollama');
|
|
590
|
+
const ollama = createOllama({
|
|
591
|
+
baseURL: config.baseUrl ?? "http://127.0.0.1:11434"
|
|
592
|
+
});
|
|
593
|
+
return { model: ollama(config.model), providerType: "ollama", config };
|
|
594
|
+
}
|
|
595
|
+
case "openai": {
|
|
596
|
+
const { createOpenAI } = await import('@ai-sdk/openai');
|
|
597
|
+
const openai = createOpenAI({
|
|
598
|
+
apiKey: config.apiKey,
|
|
599
|
+
...config.baseUrl ? { baseURL: config.baseUrl } : {}
|
|
600
|
+
});
|
|
601
|
+
return { model: openai(config.model), providerType: "openai", config };
|
|
602
|
+
}
|
|
603
|
+
case "openai-compatible": {
|
|
604
|
+
const { createOpenAICompatible } = await import('@ai-sdk/openai-compatible');
|
|
605
|
+
const provider = createOpenAICompatible({
|
|
606
|
+
baseURL: config.baseUrl ?? "http://127.0.0.1:11434/v1",
|
|
607
|
+
name: "custom",
|
|
608
|
+
...config.apiKey ? { headers: { Authorization: `Bearer ${config.apiKey}` } } : {}
|
|
609
|
+
});
|
|
610
|
+
return {
|
|
611
|
+
model: provider(config.model),
|
|
612
|
+
providerType: "openai-compatible",
|
|
613
|
+
config
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
default: {
|
|
617
|
+
throw new Error(
|
|
618
|
+
`Unsupported provider: "${config.provider}". Expected "ollama", "openai", or "openai-compatible".`
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/ai/system-prompt.ts
|
|
625
|
+
function buildSystemPrompt(viewMode, contextJson) {
|
|
626
|
+
return `You are an OpenTelemetry expert assistant analyzing live telemetry data from a running application.
|
|
627
|
+
The user is viewing their ${viewMode} dashboard in a terminal TUI.
|
|
628
|
+
|
|
629
|
+
You have tools to query the telemetry data precisely. Use them to answer questions:
|
|
630
|
+
- getOverviewStats: high-level stats (spans, errors, latency)
|
|
631
|
+
- listServices: all services with error rates and p95
|
|
632
|
+
- findSlowestSpans: find slow spans, optionally by service
|
|
633
|
+
- findErrorTraces: find traces with errors
|
|
634
|
+
- getTraceDetail: deep dive into a specific trace
|
|
635
|
+
- searchSpans: search spans by name
|
|
636
|
+
- searchLogs: search logs by message content
|
|
637
|
+
|
|
638
|
+
Use tools first to gather data, then synthesize a concise answer.
|
|
639
|
+
Keep responses under 300 words.
|
|
640
|
+
Use specific span names, durations, and attribute values from the data.
|
|
641
|
+
Format for a narrow terminal column \u2014 use short paragraphs, not wide tables.
|
|
642
|
+
|
|
643
|
+
Current dashboard summary:
|
|
644
|
+
${contextJson}`;
|
|
645
|
+
}
|
|
646
|
+
var t = ai.tool;
|
|
647
|
+
function createTelemetryTools(ctx) {
|
|
648
|
+
return {
|
|
649
|
+
getOverviewStats: t({
|
|
650
|
+
description: "Get high-level stats: total spans, error count, average duration, p95 duration, and service count.",
|
|
651
|
+
parameters: zod.z.object({}),
|
|
652
|
+
execute: async () => ({
|
|
653
|
+
totalSpans: ctx.stats.total,
|
|
654
|
+
errors: ctx.stats.errors,
|
|
655
|
+
avgMs: Math.round(ctx.stats.avg),
|
|
656
|
+
p95Ms: Math.round(ctx.stats.p95),
|
|
657
|
+
serviceCount: ctx.serviceStats.length,
|
|
658
|
+
traceCount: ctx.traces.length
|
|
659
|
+
})
|
|
660
|
+
}),
|
|
661
|
+
listServices: t({
|
|
662
|
+
description: "List all services with their span counts, error counts, and p95 latencies.",
|
|
663
|
+
parameters: zod.z.object({}),
|
|
664
|
+
execute: async () => ctx.serviceStats.map((s) => ({
|
|
665
|
+
serviceName: s.serviceName,
|
|
666
|
+
total: s.total,
|
|
667
|
+
errors: s.errors,
|
|
668
|
+
avgMs: Math.round(s.avgMs),
|
|
669
|
+
p95Ms: Math.round(s.p95Ms)
|
|
670
|
+
}))
|
|
671
|
+
}),
|
|
672
|
+
findSlowestSpans: t({
|
|
673
|
+
description: "Find the slowest spans, optionally filtered by service name. Returns span name, duration, status, and key attributes.",
|
|
674
|
+
parameters: zod.z.object({
|
|
675
|
+
service: zod.z.string().optional().describe("Filter by service name"),
|
|
676
|
+
limit: zod.z.number().optional().describe("Max results (default 10)")
|
|
677
|
+
}),
|
|
678
|
+
execute: async ({
|
|
679
|
+
service,
|
|
680
|
+
limit
|
|
681
|
+
}) => {
|
|
682
|
+
const max = limit ?? 10;
|
|
683
|
+
let filtered = ctx.spans;
|
|
684
|
+
if (service) {
|
|
685
|
+
filtered = filtered.filter(
|
|
686
|
+
(s) => s.attributes?.["service.name"] === service
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
return filtered.toSorted((a, b) => b.durationMs - a.durationMs).slice(0, max).map((s) => ({
|
|
690
|
+
name: s.name,
|
|
691
|
+
durationMs: Math.round(s.durationMs),
|
|
692
|
+
status: s.status,
|
|
693
|
+
service: s.attributes?.["service.name"] ?? "unknown",
|
|
694
|
+
route: s.attributes?.["http.route"],
|
|
695
|
+
traceId: s.traceId.slice(0, 8)
|
|
696
|
+
}));
|
|
697
|
+
}
|
|
698
|
+
}),
|
|
699
|
+
findErrorTraces: t({
|
|
700
|
+
description: "Find traces that contain errors, with root span name, service, route, and error count.",
|
|
701
|
+
parameters: zod.z.object({
|
|
702
|
+
service: zod.z.string().optional().describe("Filter by service name"),
|
|
703
|
+
limit: zod.z.number().optional().describe("Max results (default 10)")
|
|
704
|
+
}),
|
|
705
|
+
execute: async ({
|
|
706
|
+
service,
|
|
707
|
+
limit
|
|
708
|
+
}) => {
|
|
709
|
+
const max = limit ?? 10;
|
|
710
|
+
let errors = ctx.errorSummaries;
|
|
711
|
+
if (service) {
|
|
712
|
+
errors = errors.filter((e) => e.serviceName === service);
|
|
713
|
+
}
|
|
714
|
+
return errors.slice(0, max).map((e) => ({
|
|
715
|
+
traceId: e.traceId.slice(0, 8),
|
|
716
|
+
rootName: e.rootName,
|
|
717
|
+
serviceName: e.serviceName,
|
|
718
|
+
route: e.route,
|
|
719
|
+
errorCount: e.errorCount
|
|
720
|
+
}));
|
|
721
|
+
}
|
|
722
|
+
}),
|
|
723
|
+
getTraceDetail: t({
|
|
724
|
+
description: "Get full detail of a specific trace by trace ID prefix. Returns all spans with their parent relationships, durations, and attributes.",
|
|
725
|
+
parameters: zod.z.object({
|
|
726
|
+
traceIdPrefix: zod.z.string().describe("First 8+ characters of the trace ID")
|
|
727
|
+
}),
|
|
728
|
+
execute: async ({ traceIdPrefix }) => {
|
|
729
|
+
const trace = ctx.traces.find(
|
|
730
|
+
(t2) => t2.traceId.startsWith(traceIdPrefix)
|
|
731
|
+
);
|
|
732
|
+
if (!trace) {
|
|
733
|
+
return { error: `No trace found matching ${traceIdPrefix}` };
|
|
734
|
+
}
|
|
735
|
+
const traceLogs = ctx.logs.filter((l) => l.traceId === trace.traceId);
|
|
736
|
+
return {
|
|
737
|
+
traceId: trace.traceId.slice(0, 16),
|
|
738
|
+
rootName: trace.rootName,
|
|
739
|
+
durationMs: Math.round(trace.durationMs),
|
|
740
|
+
hasError: trace.hasError,
|
|
741
|
+
spanCount: trace.spanCount,
|
|
742
|
+
spans: trace.spans.map((s) => ({
|
|
743
|
+
name: s.name,
|
|
744
|
+
durationMs: Math.round(s.durationMs),
|
|
745
|
+
status: s.status,
|
|
746
|
+
kind: s.kind,
|
|
747
|
+
parentSpanId: s.parentSpanId?.slice(0, 8),
|
|
748
|
+
attrs: Object.fromEntries(
|
|
749
|
+
Object.entries(s.attributes ?? {}).filter(
|
|
750
|
+
([k]) => [
|
|
751
|
+
"http.method",
|
|
752
|
+
"http.route",
|
|
753
|
+
"http.status_code",
|
|
754
|
+
"db.operation",
|
|
755
|
+
"db.system",
|
|
756
|
+
"service.name",
|
|
757
|
+
"error.message",
|
|
758
|
+
"error.type"
|
|
759
|
+
].includes(k)
|
|
760
|
+
)
|
|
761
|
+
)
|
|
762
|
+
})),
|
|
763
|
+
logs: traceLogs.slice(0, 10).map((l) => ({
|
|
764
|
+
level: l.level,
|
|
765
|
+
message: l.message.slice(0, 100)
|
|
766
|
+
}))
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
}),
|
|
770
|
+
searchSpans: t({
|
|
771
|
+
description: "Search spans by name pattern (case-insensitive substring match). Returns matching spans with details.",
|
|
772
|
+
parameters: zod.z.object({
|
|
773
|
+
query: zod.z.string().describe("Search string to match against span names"),
|
|
774
|
+
limit: zod.z.number().optional().describe("Max results (default 20)")
|
|
775
|
+
}),
|
|
776
|
+
execute: async ({ query, limit }) => {
|
|
777
|
+
const max = limit ?? 20;
|
|
778
|
+
const q = query.toLowerCase();
|
|
779
|
+
return ctx.spans.filter((s) => s.name.toLowerCase().includes(q)).slice(0, max).map((s) => ({
|
|
780
|
+
name: s.name,
|
|
781
|
+
durationMs: Math.round(s.durationMs),
|
|
782
|
+
status: s.status,
|
|
783
|
+
traceId: s.traceId.slice(0, 8),
|
|
784
|
+
service: s.attributes?.["service.name"] ?? "unknown"
|
|
785
|
+
}));
|
|
786
|
+
}
|
|
787
|
+
}),
|
|
788
|
+
searchLogs: t({
|
|
789
|
+
description: "Search logs by message content (case-insensitive). Returns matching log entries.",
|
|
790
|
+
parameters: zod.z.object({
|
|
791
|
+
query: zod.z.string().describe("Search string to match against log messages"),
|
|
792
|
+
level: zod.z.enum(["debug", "info", "warn", "error"]).optional().describe("Filter by log level"),
|
|
793
|
+
limit: zod.z.number().optional().describe("Max results (default 20)")
|
|
794
|
+
}),
|
|
795
|
+
execute: async ({
|
|
796
|
+
query,
|
|
797
|
+
level,
|
|
798
|
+
limit
|
|
799
|
+
}) => {
|
|
800
|
+
const max = limit ?? 20;
|
|
801
|
+
const q = query.toLowerCase();
|
|
802
|
+
let filtered = ctx.logs.filter(
|
|
803
|
+
(l) => l.message.toLowerCase().includes(q)
|
|
804
|
+
);
|
|
805
|
+
if (level) {
|
|
806
|
+
filtered = filtered.filter((l) => l.level === level);
|
|
807
|
+
}
|
|
808
|
+
return filtered.slice(0, max).map((l) => ({
|
|
809
|
+
level: l.level,
|
|
810
|
+
message: l.message.slice(0, 200),
|
|
811
|
+
traceId: l.traceId?.slice(0, 8),
|
|
812
|
+
attrs: l.attributes
|
|
813
|
+
}));
|
|
814
|
+
}
|
|
815
|
+
})
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/ai/stream.ts
|
|
820
|
+
async function providerStreamText(providerType, params) {
|
|
821
|
+
if (providerType === "ollama") {
|
|
822
|
+
const mod2 = await import('ai-sdk-ollama');
|
|
823
|
+
const result = await mod2.streamText(
|
|
824
|
+
params
|
|
825
|
+
);
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
const mod = await import('ai');
|
|
829
|
+
return mod.streamText(
|
|
830
|
+
params
|
|
831
|
+
);
|
|
832
|
+
}
|
|
531
833
|
var KEY_ATTR_KEYS = /* @__PURE__ */ new Set([
|
|
532
834
|
"http.route",
|
|
533
835
|
"http.method",
|
|
@@ -550,7 +852,8 @@ function Dashboard({
|
|
|
550
852
|
maxSpans,
|
|
551
853
|
colors,
|
|
552
854
|
stream,
|
|
553
|
-
logStream
|
|
855
|
+
logStream,
|
|
856
|
+
aiConfig
|
|
554
857
|
}) {
|
|
555
858
|
const [paused, setPaused] = react.useState(false);
|
|
556
859
|
const [recording, setRecording] = react.useState(false);
|
|
@@ -563,13 +866,42 @@ function Dashboard({
|
|
|
563
866
|
const [spanFilters, setSpanFilters] = react.useState({
|
|
564
867
|
statusGroup: "all"
|
|
565
868
|
});
|
|
566
|
-
const [
|
|
567
|
-
const [
|
|
869
|
+
const [drilldownTraceId, setDrilldownTraceId] = react.useState(null);
|
|
870
|
+
const [drilldownSelectedIndex, setDrilldownSelectedIndex] = react.useState(0);
|
|
871
|
+
const [drilldownTab, setDrilldownTab] = react.useState("timeline");
|
|
568
872
|
const [newErrorCount, setNewErrorCount] = react.useState(0);
|
|
569
873
|
const [searchMode, setSearchMode] = react.useState(false);
|
|
874
|
+
const [traceIdMode, setTraceIdMode] = react.useState(false);
|
|
875
|
+
const [traceIdInput, setTraceIdInput] = react.useState("");
|
|
570
876
|
const throttleRef = react.useRef(null);
|
|
571
877
|
const pendingSpansRef = react.useRef([]);
|
|
572
878
|
const [logs, setLogs] = react.useState([]);
|
|
879
|
+
const [aiActive, setAiActive] = react.useState(false);
|
|
880
|
+
const [aiMessages, setAiMessages] = react.useState([]);
|
|
881
|
+
const [aiInput, setAiInput] = react.useState("");
|
|
882
|
+
const [aiState, setAiState] = react.useState({ status: "unconfigured" });
|
|
883
|
+
const [aiInputMode, setAiInputMode] = react.useState(false);
|
|
884
|
+
const aiModelRef = react.useRef(null);
|
|
885
|
+
const aiAbortRef = react.useRef(null);
|
|
886
|
+
react.useEffect(() => {
|
|
887
|
+
let cancelled = false;
|
|
888
|
+
resolveConfigWithAutoDetect(aiConfig).then(async (config) => {
|
|
889
|
+
if (cancelled || !config) return;
|
|
890
|
+
try {
|
|
891
|
+
const result = await createAIModel(config);
|
|
892
|
+
aiModelRef.current = result;
|
|
893
|
+
setAiState({ status: "idle" });
|
|
894
|
+
} catch {
|
|
895
|
+
setAiState({
|
|
896
|
+
status: "error",
|
|
897
|
+
message: "Failed to initialize AI model"
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
return () => {
|
|
902
|
+
cancelled = true;
|
|
903
|
+
};
|
|
904
|
+
}, [aiConfig]);
|
|
573
905
|
react.useEffect(() => {
|
|
574
906
|
if (!logStream) return;
|
|
575
907
|
const unsubscribe = logStream.onLog((event) => {
|
|
@@ -601,13 +933,16 @@ function Dashboard({
|
|
|
601
933
|
return next.slice(0, maxSpans);
|
|
602
934
|
});
|
|
603
935
|
setSelected(0);
|
|
604
|
-
|
|
936
|
+
setDrilldownTraceId(null);
|
|
605
937
|
};
|
|
606
938
|
const unsubscribe = stream.onSpanEnd((span) => {
|
|
607
939
|
if (paused) return;
|
|
608
940
|
if (span.status === "ERROR") {
|
|
609
941
|
setNewErrorCount((n) => n + 1);
|
|
610
|
-
setTimeout(
|
|
942
|
+
setTimeout(
|
|
943
|
+
() => setNewErrorCount((n) => Math.max(0, n - 1)),
|
|
944
|
+
NEW_ERROR_DISPLAY_MS
|
|
945
|
+
);
|
|
611
946
|
}
|
|
612
947
|
if (recording) {
|
|
613
948
|
setSpans((prev) => {
|
|
@@ -619,7 +954,7 @@ function Dashboard({
|
|
|
619
954
|
return next.slice(0, RECORD_LIMIT_DEFAULT);
|
|
620
955
|
});
|
|
621
956
|
setSelected(0);
|
|
622
|
-
|
|
957
|
+
setDrilldownTraceId(null);
|
|
623
958
|
return;
|
|
624
959
|
}
|
|
625
960
|
pendingSpansRef.current = [span, ...pendingSpansRef.current];
|
|
@@ -650,21 +985,24 @@ function Dashboard({
|
|
|
650
985
|
() => buildTraceSummaries(traceMap),
|
|
651
986
|
[traceMap]
|
|
652
987
|
);
|
|
653
|
-
const filteredSummaries = react.useMemo(
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
);
|
|
988
|
+
const filteredSummaries = react.useMemo(() => {
|
|
989
|
+
const base = filterTraceSummaries(traceSummaries, "");
|
|
990
|
+
return spanFilters.traceId ? base.filter((t2) => t2.traceId === spanFilters.traceId) : base;
|
|
991
|
+
}, [traceSummaries, spanFilters.traceId]);
|
|
657
992
|
const filteredSpans = react.useMemo(
|
|
658
993
|
() => filterBySearch(filteredSpanBuffer, ""),
|
|
659
994
|
[filteredSpanBuffer]
|
|
660
995
|
);
|
|
661
996
|
const stats = react.useMemo(() => computeStats(spans), [spans]);
|
|
662
|
-
const perSpanNameStats = react.useMemo(
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
() => filterLogsBySearch(logs, searchQuery),
|
|
666
|
-
[logs, searchQuery]
|
|
997
|
+
const perSpanNameStats = react.useMemo(
|
|
998
|
+
() => computePerSpanNameStats(spans),
|
|
999
|
+
[spans]
|
|
667
1000
|
);
|
|
1001
|
+
const logStats = react.useMemo(() => computeLogStats(logs), [logs]);
|
|
1002
|
+
const filteredLogs = react.useMemo(() => {
|
|
1003
|
+
const traceFilteredLogs = spanFilters.traceId ? logs.filter((l) => l.traceId === spanFilters.traceId) : logs;
|
|
1004
|
+
return filterLogsBySearch(traceFilteredLogs, searchQuery);
|
|
1005
|
+
}, [logs, searchQuery, spanFilters.traceId]);
|
|
668
1006
|
const serviceStats = react.useMemo(() => computeServiceStats(spans), [spans]);
|
|
669
1007
|
const selectedServiceName = serviceStats[selected]?.serviceName ?? null;
|
|
670
1008
|
const spansForSelectedService = react.useMemo(() => {
|
|
@@ -681,24 +1019,147 @@ function Dashboard({
|
|
|
681
1019
|
() => findHotSpanNames(spansForSelectedService, 8),
|
|
682
1020
|
[spansForSelectedService]
|
|
683
1021
|
);
|
|
684
|
-
const selectedTraceSummary =
|
|
1022
|
+
const selectedTraceSummary = drilldownTraceId == null ? filteredSummaries[selected] ?? null : filteredSummaries.find((t2) => t2.traceId === drilldownTraceId) ?? null;
|
|
685
1023
|
const errorSummaries = react.useMemo(
|
|
686
1024
|
() => buildErrorSummaries(traceSummaries),
|
|
687
1025
|
[traceSummaries]
|
|
688
1026
|
);
|
|
1027
|
+
const filteredErrorSummaries = spanFilters.traceId ? errorSummaries.filter((e) => e.traceId === spanFilters.traceId) : errorSummaries;
|
|
689
1028
|
const traceTree = selectedTraceSummary == null ? [] : flattenTraceTree(buildTraceTree(selectedTraceSummary.spans));
|
|
690
1029
|
const waterfallSpans = selectedTraceSummary == null ? [] : sortSpansForWaterfall(selectedTraceSummary.spans);
|
|
691
|
-
const currentSpanInTrace = traceTree[
|
|
1030
|
+
const currentSpanInTrace = traceTree[drilldownSelectedIndex] ?? null;
|
|
692
1031
|
const currentSpanInFlat = filteredSpans[selected] ?? null;
|
|
693
|
-
const selectedTraceSummaryForDetails = viewMode === "trace" &&
|
|
1032
|
+
const selectedTraceSummaryForDetails = viewMode === "trace" && drilldownTraceId == null && filteredSummaries[selected] ? filteredSummaries[selected] : null;
|
|
694
1033
|
const rootSpanOfSelectedTrace = selectedTraceSummaryForDetails != null && selectedTraceSummaryForDetails.spans.length > 0 ? selectedTraceSummaryForDetails.spans.find(
|
|
695
1034
|
(s) => !selectedTraceSummaryForDetails.spans.some(
|
|
696
1035
|
(p) => p.spanId === s.parentSpanId
|
|
697
1036
|
)
|
|
698
1037
|
) ?? selectedTraceSummaryForDetails.spans[0] : null;
|
|
699
|
-
const currentSpan = viewMode === "trace" ?
|
|
1038
|
+
const currentSpan = viewMode === "trace" ? drilldownTraceId == null ? rootSpanOfSelectedTrace ?? null : currentSpanInTrace?.span ?? null : viewMode === "span" ? currentSpanInFlat : null;
|
|
700
1039
|
const selectedTraceLogs = selectedTraceSummary?.traceId && logs.length > 0 ? logs.filter((l) => l.traceId === selectedTraceSummary.traceId) : [];
|
|
701
1040
|
const timelineItems = selectedTraceSummary && (selectedTraceSummary.spans.length > 0 || selectedTraceLogs.length > 0) ? buildTraceTimeline(selectedTraceSummary.spans, selectedTraceLogs) : [];
|
|
1041
|
+
const drilldownSpans = react.useMemo(
|
|
1042
|
+
() => drilldownTraceId ? spans.filter((s) => s.traceId === drilldownTraceId) : [],
|
|
1043
|
+
[spans, drilldownTraceId]
|
|
1044
|
+
);
|
|
1045
|
+
const drilldownTree = react.useMemo(
|
|
1046
|
+
() => drilldownSpans.length > 0 ? flattenTraceTree(buildTraceTree(drilldownSpans)) : [],
|
|
1047
|
+
[drilldownSpans]
|
|
1048
|
+
);
|
|
1049
|
+
const drilldownLogs = react.useMemo(
|
|
1050
|
+
() => drilldownTraceId ? logs.filter((l) => l.traceId === drilldownTraceId) : [],
|
|
1051
|
+
[logs, drilldownTraceId]
|
|
1052
|
+
);
|
|
1053
|
+
const drilldownTimeline = react.useMemo(
|
|
1054
|
+
() => drilldownSpans.length > 0 || drilldownLogs.length > 0 ? buildTraceTimeline(drilldownSpans, drilldownLogs) : [],
|
|
1055
|
+
[drilldownSpans, drilldownLogs]
|
|
1056
|
+
);
|
|
1057
|
+
const drilldownSummary = react.useMemo(
|
|
1058
|
+
() => drilldownTraceId ? traceSummaries.find((t2) => t2.traceId === drilldownTraceId) ?? null : null,
|
|
1059
|
+
[traceSummaries, drilldownTraceId]
|
|
1060
|
+
);
|
|
1061
|
+
const drilldownSelectedItem = react.useMemo(() => {
|
|
1062
|
+
if (!drilldownTraceId) return null;
|
|
1063
|
+
if (drilldownTab === "timeline") {
|
|
1064
|
+
const item = drilldownTimeline[drilldownSelectedIndex];
|
|
1065
|
+
if (!item) return null;
|
|
1066
|
+
return item;
|
|
1067
|
+
}
|
|
1068
|
+
if (drilldownTab === "spans") {
|
|
1069
|
+
return drilldownTree[drilldownSelectedIndex] ? {
|
|
1070
|
+
type: "span",
|
|
1071
|
+
span: drilldownTree[drilldownSelectedIndex].span
|
|
1072
|
+
} : null;
|
|
1073
|
+
}
|
|
1074
|
+
if (drilldownTab === "logs") {
|
|
1075
|
+
return drilldownLogs[drilldownSelectedIndex] ? { type: "log", log: drilldownLogs[drilldownSelectedIndex] } : null;
|
|
1076
|
+
}
|
|
1077
|
+
return null;
|
|
1078
|
+
}, [
|
|
1079
|
+
drilldownTraceId,
|
|
1080
|
+
drilldownTab,
|
|
1081
|
+
drilldownSelectedIndex,
|
|
1082
|
+
drilldownTimeline,
|
|
1083
|
+
drilldownTree,
|
|
1084
|
+
drilldownLogs
|
|
1085
|
+
]);
|
|
1086
|
+
const sendAIQuery = async (question) => {
|
|
1087
|
+
const aiResult = aiModelRef.current;
|
|
1088
|
+
if (!aiResult || aiState.status === "streaming") return;
|
|
1089
|
+
const userMsg = { role: "user", content: question };
|
|
1090
|
+
setAiMessages((prev) => [...prev, userMsg]);
|
|
1091
|
+
setAiInput("");
|
|
1092
|
+
const abort = new AbortController();
|
|
1093
|
+
aiAbortRef.current = abort;
|
|
1094
|
+
setAiState({ status: "streaming", abortController: abort });
|
|
1095
|
+
const toolCtx = {
|
|
1096
|
+
spans,
|
|
1097
|
+
logs,
|
|
1098
|
+
traces: traceSummaries,
|
|
1099
|
+
stats,
|
|
1100
|
+
serviceStats,
|
|
1101
|
+
errorSummaries
|
|
1102
|
+
};
|
|
1103
|
+
const tools = createTelemetryTools(toolCtx);
|
|
1104
|
+
const statsContext = JSON.stringify({
|
|
1105
|
+
viewMode,
|
|
1106
|
+
stats: {
|
|
1107
|
+
totalSpans: stats.total,
|
|
1108
|
+
errors: stats.errors,
|
|
1109
|
+
avgMs: Math.round(stats.avg),
|
|
1110
|
+
p95Ms: Math.round(stats.p95)
|
|
1111
|
+
},
|
|
1112
|
+
services: serviceStats.length,
|
|
1113
|
+
traces: traceSummaries.length
|
|
1114
|
+
});
|
|
1115
|
+
const drilldownContext = drilldownTraceId ? `
|
|
1116
|
+
|
|
1117
|
+
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"}.` : "";
|
|
1118
|
+
const systemPrompt = buildSystemPrompt(viewMode, statsContext) + drilldownContext;
|
|
1119
|
+
try {
|
|
1120
|
+
setAiMessages((prev) => [...prev, { role: "assistant", content: "" }]);
|
|
1121
|
+
const result = await providerStreamText(aiResult.providerType, {
|
|
1122
|
+
model: aiResult.model,
|
|
1123
|
+
system: systemPrompt,
|
|
1124
|
+
messages: [...aiMessages, userMsg].map((m) => ({
|
|
1125
|
+
role: m.role,
|
|
1126
|
+
content: m.content
|
|
1127
|
+
})),
|
|
1128
|
+
tools,
|
|
1129
|
+
maxSteps: 10,
|
|
1130
|
+
abortSignal: abort.signal
|
|
1131
|
+
});
|
|
1132
|
+
let fullText = "";
|
|
1133
|
+
for await (const chunk of result.textStream) {
|
|
1134
|
+
if (abort.signal.aborted) break;
|
|
1135
|
+
fullText += chunk;
|
|
1136
|
+
const captured = fullText;
|
|
1137
|
+
setAiMessages((prev) => {
|
|
1138
|
+
const updated = [...prev];
|
|
1139
|
+
const lastMsg = updated.at(-1);
|
|
1140
|
+
if (lastMsg?.role === "assistant") {
|
|
1141
|
+
updated[updated.length - 1] = {
|
|
1142
|
+
role: "assistant",
|
|
1143
|
+
content: captured
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
return updated;
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
setAiState({ status: "idle" });
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
if (abort.signal.aborted) {
|
|
1152
|
+
setAiState({ status: "idle" });
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
setAiState({
|
|
1156
|
+
status: "error",
|
|
1157
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1158
|
+
});
|
|
1159
|
+
} finally {
|
|
1160
|
+
aiAbortRef.current = null;
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
702
1163
|
const { isRawModeSupported } = ink.useStdin();
|
|
703
1164
|
ink.useInput(
|
|
704
1165
|
(input, key) => {
|
|
@@ -716,53 +1177,171 @@ function Dashboard({
|
|
|
716
1177
|
return;
|
|
717
1178
|
}
|
|
718
1179
|
if (searchMode) {
|
|
719
|
-
if (key.
|
|
720
|
-
setSearchQuery((q) => q.slice(0, -1));
|
|
721
|
-
} else if (key.return) {
|
|
1180
|
+
if (key.escape || key.return) {
|
|
722
1181
|
setSearchMode(false);
|
|
1182
|
+
} else if (key.backspace || key.delete) {
|
|
1183
|
+
setSearchQuery((q) => q.slice(0, -1));
|
|
1184
|
+
} else if (key.tab) {
|
|
1185
|
+
if (searchQuery.length >= 4) {
|
|
1186
|
+
const match = traceSummaries.find(
|
|
1187
|
+
(t2) => t2.traceId.toLowerCase().startsWith(searchQuery.toLowerCase())
|
|
1188
|
+
);
|
|
1189
|
+
if (match) {
|
|
1190
|
+
setSpanFilters((prev) => ({ ...prev, traceId: match.traceId }));
|
|
1191
|
+
setSearchMode(false);
|
|
1192
|
+
setSearchQuery("");
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
723
1195
|
} else if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
724
1196
|
setSearchQuery((q) => q + input);
|
|
725
1197
|
}
|
|
726
1198
|
return;
|
|
727
1199
|
}
|
|
728
|
-
if (
|
|
729
|
-
if (
|
|
730
|
-
|
|
731
|
-
|
|
1200
|
+
if (traceIdMode) {
|
|
1201
|
+
if (key.escape) {
|
|
1202
|
+
setTraceIdMode(false);
|
|
1203
|
+
setTraceIdInput("");
|
|
1204
|
+
} else if (key.return) {
|
|
1205
|
+
if (traceIdInput.trim()) {
|
|
1206
|
+
const match = traceSummaries.find(
|
|
1207
|
+
(t2) => t2.traceId.toLowerCase().startsWith(traceIdInput.toLowerCase())
|
|
1208
|
+
);
|
|
1209
|
+
if (match) {
|
|
1210
|
+
setSpanFilters((prev) => ({ ...prev, traceId: match.traceId }));
|
|
1211
|
+
}
|
|
1212
|
+
} else {
|
|
1213
|
+
setSpanFilters((prev) => {
|
|
1214
|
+
const { traceId: _, ...rest } = prev;
|
|
1215
|
+
return rest;
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
setTraceIdMode(false);
|
|
1219
|
+
setTraceIdInput("");
|
|
1220
|
+
} else if (key.tab) {
|
|
1221
|
+
if (traceIdInput.length >= 2) {
|
|
1222
|
+
const match = traceSummaries.find(
|
|
1223
|
+
(t2) => t2.traceId.toLowerCase().startsWith(traceIdInput.toLowerCase())
|
|
1224
|
+
);
|
|
1225
|
+
if (match) {
|
|
1226
|
+
setTraceIdInput(match.traceId);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
} else if (key.backspace || key.delete) {
|
|
1230
|
+
setTraceIdInput((q) => q.slice(0, -1));
|
|
1231
|
+
} else if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
1232
|
+
setTraceIdInput((q) => q + input);
|
|
1233
|
+
}
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
if (aiInputMode) {
|
|
1237
|
+
if (key.escape) {
|
|
1238
|
+
if (aiState.status === "streaming") {
|
|
1239
|
+
aiAbortRef.current?.abort();
|
|
1240
|
+
} else {
|
|
1241
|
+
setAiInputMode(false);
|
|
1242
|
+
setAiActive(false);
|
|
1243
|
+
}
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
if (key.backspace || key.delete) {
|
|
1247
|
+
setAiInput((q) => q.slice(0, -1));
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (key.return) {
|
|
1251
|
+
if (aiInput.trim()) {
|
|
1252
|
+
sendAIQuery(aiInput.trim());
|
|
1253
|
+
}
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
1257
|
+
setAiInput((q) => q + input);
|
|
1258
|
+
}
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
if (input === "a") {
|
|
1262
|
+
setAiActive((v) => !v);
|
|
1263
|
+
if (aiActive) {
|
|
1264
|
+
setAiInputMode(false);
|
|
732
1265
|
} else {
|
|
1266
|
+
if (aiState.status !== "unconfigured") {
|
|
1267
|
+
setAiInputMode(true);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
if (key.escape) {
|
|
1273
|
+
if (drilldownTraceId == null) {
|
|
733
1274
|
setSearchMode(false);
|
|
1275
|
+
} else {
|
|
1276
|
+
setDrilldownTraceId(null);
|
|
1277
|
+
setDrilldownSelectedIndex(0);
|
|
1278
|
+
setDrilldownTab("timeline");
|
|
734
1279
|
}
|
|
735
1280
|
return;
|
|
736
1281
|
}
|
|
737
|
-
if (key.
|
|
738
|
-
|
|
739
|
-
|
|
1282
|
+
if (key.tab && drilldownTraceId != null) {
|
|
1283
|
+
const tabs = [
|
|
1284
|
+
"timeline",
|
|
1285
|
+
"spans",
|
|
1286
|
+
"logs"
|
|
1287
|
+
];
|
|
1288
|
+
const currentIdx = tabs.indexOf(drilldownTab);
|
|
1289
|
+
const nextIdx = key.shift ? (currentIdx - 1 + tabs.length) % tabs.length : (currentIdx + 1) % tabs.length;
|
|
1290
|
+
setDrilldownTab(tabs[nextIdx]);
|
|
1291
|
+
setDrilldownSelectedIndex(0);
|
|
740
1292
|
return;
|
|
741
1293
|
}
|
|
1294
|
+
if (key.return && drilldownTraceId == null) {
|
|
1295
|
+
let targetTraceId;
|
|
1296
|
+
let preSelectIndex = 0;
|
|
1297
|
+
if (viewMode === "trace" && filteredSummaries[selected]) {
|
|
1298
|
+
targetTraceId = filteredSummaries[selected].traceId;
|
|
1299
|
+
} else if (viewMode === "log" && filteredLogs[selected]?.traceId) {
|
|
1300
|
+
targetTraceId = filteredLogs[selected].traceId;
|
|
1301
|
+
const originLog = filteredLogs[selected];
|
|
1302
|
+
const traceSpans = spans.filter((s) => s.traceId === targetTraceId);
|
|
1303
|
+
const traceLogs = logs.filter((l) => l.traceId === targetTraceId);
|
|
1304
|
+
const timeline = buildTraceTimeline(traceSpans, traceLogs);
|
|
1305
|
+
preSelectIndex = timeline.findIndex(
|
|
1306
|
+
(item) => item.type === "log" && item.log === originLog
|
|
1307
|
+
);
|
|
1308
|
+
if (preSelectIndex < 0) preSelectIndex = 0;
|
|
1309
|
+
} else if (viewMode === "span" && filteredSpans[selected]?.traceId) {
|
|
1310
|
+
targetTraceId = filteredSpans[selected].traceId;
|
|
1311
|
+
const originSpan = filteredSpans[selected];
|
|
1312
|
+
const traceSpans = spans.filter((s) => s.traceId === targetTraceId);
|
|
1313
|
+
const traceLogs = logs.filter((l) => l.traceId === targetTraceId);
|
|
1314
|
+
const timeline = buildTraceTimeline(traceSpans, traceLogs);
|
|
1315
|
+
preSelectIndex = timeline.findIndex(
|
|
1316
|
+
(item) => item.type === "span" && item.span?.spanId === originSpan.spanId
|
|
1317
|
+
);
|
|
1318
|
+
if (preSelectIndex < 0) preSelectIndex = 0;
|
|
1319
|
+
}
|
|
1320
|
+
if (targetTraceId) {
|
|
1321
|
+
setDrilldownTraceId(targetTraceId);
|
|
1322
|
+
setDrilldownSelectedIndex(preSelectIndex);
|
|
1323
|
+
setDrilldownTab("timeline");
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
742
1327
|
if (key.upArrow || key.downArrow) {
|
|
1328
|
+
if (drilldownTraceId != null) {
|
|
1329
|
+
const listLength = drilldownTab === "timeline" ? drilldownTimeline.length : drilldownTab === "spans" ? drilldownTree.length : drilldownLogs.length;
|
|
1330
|
+
if (key.upArrow) {
|
|
1331
|
+
setDrilldownSelectedIndex((i) => Math.max(0, i - 1));
|
|
1332
|
+
} else {
|
|
1333
|
+
setDrilldownSelectedIndex((i) => Math.min(listLength - 1, i + 1));
|
|
1334
|
+
}
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
743
1337
|
switch (viewMode) {
|
|
744
1338
|
case "trace": {
|
|
745
1339
|
if (key.upArrow) {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
} else {
|
|
749
|
-
setSelected((i) => Math.max(0, i - 1));
|
|
750
|
-
setSelectedSpanIndex(0);
|
|
751
|
-
}
|
|
1340
|
+
setSelected((i) => Math.max(0, i - 1));
|
|
1341
|
+
setDrilldownSelectedIndex(0);
|
|
752
1342
|
} 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
|
-
}
|
|
1343
|
+
setSelected((i) => Math.min(filteredSummaries.length - 1, i + 1));
|
|
1344
|
+
setDrilldownSelectedIndex(0);
|
|
766
1345
|
}
|
|
767
1346
|
break;
|
|
768
1347
|
}
|
|
@@ -794,7 +1373,9 @@ function Dashboard({
|
|
|
794
1373
|
if (key.upArrow) {
|
|
795
1374
|
setSelected((i) => Math.max(0, i - 1));
|
|
796
1375
|
} else if (key.downArrow) {
|
|
797
|
-
setSelected(
|
|
1376
|
+
setSelected(
|
|
1377
|
+
(i) => Math.min(filteredErrorSummaries.length - 1, i + 1)
|
|
1378
|
+
);
|
|
798
1379
|
}
|
|
799
1380
|
break;
|
|
800
1381
|
}
|
|
@@ -805,35 +1386,35 @@ function Dashboard({
|
|
|
805
1386
|
if (input === "t") {
|
|
806
1387
|
setViewMode((m) => m === "trace" ? "span" : "trace");
|
|
807
1388
|
setSelected(0);
|
|
808
|
-
|
|
809
|
-
|
|
1389
|
+
setDrilldownTraceId(null);
|
|
1390
|
+
setDrilldownSelectedIndex(0);
|
|
810
1391
|
}
|
|
811
1392
|
if (input === "l") {
|
|
812
1393
|
setViewMode((m) => m === "log" ? "trace" : "log");
|
|
813
1394
|
setSelected(0);
|
|
814
|
-
|
|
815
|
-
|
|
1395
|
+
setDrilldownTraceId(null);
|
|
1396
|
+
setDrilldownSelectedIndex(0);
|
|
816
1397
|
}
|
|
817
1398
|
if (input === "v") {
|
|
818
1399
|
setViewMode(
|
|
819
1400
|
(m) => m === "service-summary" ? "trace" : "service-summary"
|
|
820
1401
|
);
|
|
821
1402
|
setSelected(0);
|
|
822
|
-
|
|
823
|
-
|
|
1403
|
+
setDrilldownTraceId(null);
|
|
1404
|
+
setDrilldownSelectedIndex(0);
|
|
824
1405
|
}
|
|
825
1406
|
if (input === "E") {
|
|
826
1407
|
setViewMode((m) => m === "errors" ? "trace" : "errors");
|
|
827
1408
|
setSelected(0);
|
|
828
|
-
|
|
829
|
-
|
|
1409
|
+
setDrilldownTraceId(null);
|
|
1410
|
+
setDrilldownSelectedIndex(0);
|
|
830
1411
|
}
|
|
831
1412
|
if (input === "c") {
|
|
832
1413
|
setSpans([]);
|
|
833
1414
|
setLogs([]);
|
|
834
1415
|
setSelected(0);
|
|
835
|
-
|
|
836
|
-
|
|
1416
|
+
setDrilldownTraceId(null);
|
|
1417
|
+
setDrilldownSelectedIndex(0);
|
|
837
1418
|
setNewErrorCount(0);
|
|
838
1419
|
setSpanFilters({ statusGroup: "all" });
|
|
839
1420
|
setRecording(false);
|
|
@@ -843,8 +1424,8 @@ function Dashboard({
|
|
|
843
1424
|
setSpans([]);
|
|
844
1425
|
setLogs([]);
|
|
845
1426
|
setSelected(0);
|
|
846
|
-
|
|
847
|
-
|
|
1427
|
+
setDrilldownTraceId(null);
|
|
1428
|
+
setDrilldownSelectedIndex(0);
|
|
848
1429
|
setNewErrorCount(0);
|
|
849
1430
|
setSpanFilters({ statusGroup: "all" });
|
|
850
1431
|
setPaused(false);
|
|
@@ -853,8 +1434,13 @@ function Dashboard({
|
|
|
853
1434
|
if (input === "x") {
|
|
854
1435
|
setSpanFilters({ statusGroup: "all" });
|
|
855
1436
|
setSelected(0);
|
|
856
|
-
|
|
857
|
-
|
|
1437
|
+
setDrilldownTraceId(null);
|
|
1438
|
+
setDrilldownSelectedIndex(0);
|
|
1439
|
+
}
|
|
1440
|
+
if (input === "f") {
|
|
1441
|
+
setTraceIdMode(true);
|
|
1442
|
+
setTraceIdInput(spanFilters.traceId ?? "");
|
|
1443
|
+
return;
|
|
858
1444
|
}
|
|
859
1445
|
if (input === "H") {
|
|
860
1446
|
setSpanFilters((prev) => {
|
|
@@ -862,16 +1448,16 @@ function Dashboard({
|
|
|
862
1448
|
return { ...prev, statusGroup: next };
|
|
863
1449
|
});
|
|
864
1450
|
setSelected(0);
|
|
865
|
-
|
|
866
|
-
|
|
1451
|
+
setDrilldownTraceId(null);
|
|
1452
|
+
setDrilldownSelectedIndex(0);
|
|
867
1453
|
}
|
|
868
1454
|
if (input === "S") {
|
|
869
1455
|
const svc = currentSpan?.attributes?.["service.name"];
|
|
870
1456
|
if (typeof svc === "string" && svc.trim()) {
|
|
871
1457
|
setSpanFilters((prev) => ({ ...prev, serviceName: svc }));
|
|
872
1458
|
setSelected(0);
|
|
873
|
-
|
|
874
|
-
|
|
1459
|
+
setDrilldownTraceId(null);
|
|
1460
|
+
setDrilldownSelectedIndex(0);
|
|
875
1461
|
}
|
|
876
1462
|
}
|
|
877
1463
|
if (input === "R") {
|
|
@@ -879,14 +1465,74 @@ function Dashboard({
|
|
|
879
1465
|
if (typeof route === "string" && route.trim()) {
|
|
880
1466
|
setSpanFilters((prev) => ({ ...prev, route }));
|
|
881
1467
|
setSelected(0);
|
|
882
|
-
|
|
883
|
-
|
|
1468
|
+
setDrilldownTraceId(null);
|
|
1469
|
+
setDrilldownSelectedIndex(0);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
if (input === "T") {
|
|
1473
|
+
let traceId;
|
|
1474
|
+
if (drilldownTraceId) {
|
|
1475
|
+
traceId = drilldownTraceId;
|
|
1476
|
+
} else {
|
|
1477
|
+
switch (viewMode) {
|
|
1478
|
+
case "log": {
|
|
1479
|
+
traceId = filteredLogs[selected]?.traceId;
|
|
1480
|
+
break;
|
|
1481
|
+
}
|
|
1482
|
+
case "span": {
|
|
1483
|
+
traceId = filteredSpans[selected]?.traceId;
|
|
1484
|
+
break;
|
|
1485
|
+
}
|
|
1486
|
+
case "errors": {
|
|
1487
|
+
traceId = filteredErrorSummaries[selected]?.traceId;
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
if (traceId && viewMode !== "trace") {
|
|
1493
|
+
setSpanFilters((prev) => ({ ...prev, traceId }));
|
|
1494
|
+
setViewMode("trace");
|
|
1495
|
+
setSelected(0);
|
|
1496
|
+
setDrilldownTraceId(null);
|
|
1497
|
+
setDrilldownSelectedIndex(0);
|
|
1498
|
+
setDrilldownTab("timeline");
|
|
1499
|
+
}
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
if (input === "L") {
|
|
1503
|
+
let traceId;
|
|
1504
|
+
if (drilldownTraceId) {
|
|
1505
|
+
traceId = drilldownTraceId;
|
|
1506
|
+
} else {
|
|
1507
|
+
switch (viewMode) {
|
|
1508
|
+
case "trace": {
|
|
1509
|
+
traceId = filteredSummaries[selected]?.traceId;
|
|
1510
|
+
break;
|
|
1511
|
+
}
|
|
1512
|
+
case "span": {
|
|
1513
|
+
traceId = filteredSpans[selected]?.traceId;
|
|
1514
|
+
break;
|
|
1515
|
+
}
|
|
1516
|
+
case "errors": {
|
|
1517
|
+
traceId = filteredErrorSummaries[selected]?.traceId;
|
|
1518
|
+
break;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
if (traceId && viewMode !== "log") {
|
|
1523
|
+
setSpanFilters((prev) => ({ ...prev, traceId }));
|
|
1524
|
+
setViewMode("log");
|
|
1525
|
+
setSelected(0);
|
|
1526
|
+
setDrilldownTraceId(null);
|
|
1527
|
+
setDrilldownSelectedIndex(0);
|
|
1528
|
+
setDrilldownTab("timeline");
|
|
884
1529
|
}
|
|
1530
|
+
return;
|
|
885
1531
|
}
|
|
886
1532
|
if (input === "J") {
|
|
887
|
-
const
|
|
888
|
-
if (!
|
|
889
|
-
const json = exportTraceToJson(
|
|
1533
|
+
const t2 = selectedTraceSummary;
|
|
1534
|
+
if (!t2) return;
|
|
1535
|
+
const json = exportTraceToJson(t2, selectedTraceLogs);
|
|
890
1536
|
process.stdout.write(`
|
|
891
1537
|
[autotel-terminal] trace export
|
|
892
1538
|
${json}
|
|
@@ -899,22 +1545,32 @@ ${json}
|
|
|
899
1545
|
const headerModeLabel = viewMode === "trace" ? "traces" : viewMode === "span" ? "spans" : viewMode === "log" ? "logs" : viewMode === "service-summary" ? "services" : "errors";
|
|
900
1546
|
const showNewError = newErrorCount > 0;
|
|
901
1547
|
function renderTreeRow(node, index) {
|
|
902
|
-
const isSel =
|
|
1548
|
+
const isSel = drilldownTraceId != null && index === drilldownSelectedIndex;
|
|
903
1549
|
const prefix = node.depth === 0 ? "" : " ".repeat(node.depth) + (node.children.length > 0 ? "\u251C\u2500\u2500 " : "\u2514\u2500\u2500 ");
|
|
904
1550
|
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
|
-
|
|
1551
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1552
|
+
ink.Box,
|
|
1553
|
+
{
|
|
1554
|
+
flexDirection: "row",
|
|
1555
|
+
children: [
|
|
1556
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
|
|
1557
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: prefix }),
|
|
1558
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: colors ? statusColor : void 0, children: truncate(node.span.name, 24) }),
|
|
1559
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1560
|
+
" ",
|
|
1561
|
+
formatDurationMs(node.span.durationMs)
|
|
1562
|
+
] })
|
|
1563
|
+
]
|
|
1564
|
+
},
|
|
1565
|
+
`${node.span.spanId}-${node.span.startTime}`
|
|
1566
|
+
);
|
|
914
1567
|
}
|
|
915
1568
|
function keyAttrsAndRest(attrs) {
|
|
916
1569
|
if (!attrs || Object.keys(attrs).length === 0)
|
|
917
|
-
return {
|
|
1570
|
+
return {
|
|
1571
|
+
key: [],
|
|
1572
|
+
rest: []
|
|
1573
|
+
};
|
|
918
1574
|
const entries = Object.entries(attrs);
|
|
919
1575
|
const key = entries.filter(([k]) => KEY_ATTR_KEYS.has(k));
|
|
920
1576
|
const rest = entries.filter(([k]) => !KEY_ATTR_KEYS.has(k));
|
|
@@ -954,24 +1610,49 @@ ${json}
|
|
|
954
1610
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Shortcuts" }),
|
|
955
1611
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Navigation: \u2191/\u2193, Enter, Esc" }),
|
|
956
1612
|
/* @__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)" }),
|
|
1613
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Search: / (Tab autocompletes traceId)" }),
|
|
1614
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Filters: e (errors-only), S (service), R (route), H (status), f (traceId), x (clear)" }),
|
|
959
1615
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Capture: p (pause), r (record snapshot), J (export trace JSON)" }),
|
|
1616
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "AI: a (toggle AI panel)" }),
|
|
1617
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Jump: T (trace for item), L (logs for item)" }),
|
|
1618
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Drill-down: Enter (open trace), Tab (cycle tabs), Esc (back)" }),
|
|
960
1619
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Other: c (clear), ? (help), Ctrl+C (exit)" })
|
|
961
1620
|
]
|
|
962
1621
|
}
|
|
963
|
-
) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1622
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1623
|
+
ink.Box,
|
|
1624
|
+
{
|
|
1625
|
+
marginBottom: 1,
|
|
1626
|
+
flexDirection: "row",
|
|
1627
|
+
justifyContent: "space-between",
|
|
1628
|
+
children: [
|
|
1629
|
+
searchMode ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "cyan", children: [
|
|
1630
|
+
"Search: ",
|
|
1631
|
+
searchQuery || "(type to filter)",
|
|
1632
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: " (Tab: match traceId, Esc: cancel)" })
|
|
1633
|
+
] }, "search") : traceIdMode ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "yellow", children: [
|
|
1634
|
+
"TraceId: ",
|
|
1635
|
+
traceIdInput || "(type prefix, Tab to complete)",
|
|
1636
|
+
traceIdInput.length >= 2 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1637
|
+
" ",
|
|
1638
|
+
"\u2192",
|
|
1639
|
+
" ",
|
|
1640
|
+
traceSummaries.find(
|
|
1641
|
+
(t2) => t2.traceId.toLowerCase().startsWith(traceIdInput.toLowerCase())
|
|
1642
|
+
)?.traceId.slice(0, 16) ?? "no match",
|
|
1643
|
+
"\u2026"
|
|
1644
|
+
] })
|
|
1645
|
+
] }, "traceid-input") : drilldownTraceId == null ? /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "\u2191/\u2193 select \u2022 Enter open \u2022 Esc back \u2022 t spans \u2022 l logs \u2022 v svc \u2022 E errors \u2022 T trace \u2022 L logs \u2022 / search \u2022 f traceId \u2022 p pause \u2022 r record \u2022 e errors \u2022 c clear \u2022 ? help" }, "controls") : /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "\u2191/\u2193 select \u2022 Tab cycle tabs \u2022 Esc back \u2022 T trace \u2022 L logs \u2022 a AI \u2022 ? help" }, "controls"),
|
|
1646
|
+
/* @__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}/${serviceStats.length}` : viewMode === "errors" ? `errors ${filteredErrorSummaries.length}/${errorSummaries.length}` : `logs ${filteredLogs.length}/${logs.length}` }, "count")
|
|
1647
|
+
]
|
|
1648
|
+
}
|
|
1649
|
+
),
|
|
1650
|
+
(spanFilters.serviceName || spanFilters.route || spanFilters.statusGroup !== "all" || spanFilters.traceId) && /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginBottom: 1, children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
971
1651
|
"filters:",
|
|
972
1652
|
spanFilters.serviceName ? ` service=${spanFilters.serviceName}` : "",
|
|
973
1653
|
spanFilters.route ? ` route=${spanFilters.route}` : "",
|
|
974
|
-
spanFilters.statusGroup && spanFilters.statusGroup !== "all" ? ` status=${spanFilters.statusGroup}` : ""
|
|
1654
|
+
spanFilters.statusGroup && spanFilters.statusGroup !== "all" ? ` status=${spanFilters.statusGroup}` : "",
|
|
1655
|
+
spanFilters.traceId ? ` trace=${spanFilters.traceId.slice(0, 8)}\u2026` : ""
|
|
975
1656
|
] }) }),
|
|
976
1657
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", gap: 2, children: [
|
|
977
1658
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -986,51 +1667,202 @@ ${json}
|
|
|
986
1667
|
children: [
|
|
987
1668
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 0, marginBottom: 1, children: [
|
|
988
1669
|
/* @__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.
|
|
1670
|
+
filterErrorsOnly && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "red", children: [
|
|
1671
|
+
" ",
|
|
1672
|
+
"(errors only)"
|
|
1673
|
+
] }, "errors-only-label"),
|
|
990
1674
|
searchQuery && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
991
|
-
"
|
|
1675
|
+
" ",
|
|
1676
|
+
"/",
|
|
992
1677
|
searchQuery
|
|
993
1678
|
] }, "search-label")
|
|
994
1679
|
] }),
|
|
995
|
-
|
|
1680
|
+
drilldownTraceId != null && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1681
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 0, flexDirection: "row", gap: 2, children: [
|
|
1682
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "yellow", children: drilldownSummary?.rootName ?? "unknown" }),
|
|
1683
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1684
|
+
drilldownTraceId.slice(0, 16),
|
|
1685
|
+
"\u2026"
|
|
1686
|
+
] }),
|
|
1687
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "green", children: drilldownSummary ? formatDurationMs(drilldownSummary.durationMs) : "?" }),
|
|
1688
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1689
|
+
drilldownSpans.length,
|
|
1690
|
+
" spans \u2022 ",
|
|
1691
|
+
drilldownLogs.length,
|
|
1692
|
+
" logs"
|
|
1693
|
+
] })
|
|
1694
|
+
] }),
|
|
1695
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 0, flexDirection: "row", gap: 2, children: [
|
|
1696
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1697
|
+
ink.Text,
|
|
1698
|
+
{
|
|
1699
|
+
color: drilldownTab === "timeline" ? "yellow" : void 0,
|
|
1700
|
+
dimColor: drilldownTab !== "timeline",
|
|
1701
|
+
underline: drilldownTab === "timeline",
|
|
1702
|
+
children: "Timeline"
|
|
1703
|
+
}
|
|
1704
|
+
),
|
|
1705
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1706
|
+
ink.Text,
|
|
1707
|
+
{
|
|
1708
|
+
color: drilldownTab === "spans" ? "yellow" : void 0,
|
|
1709
|
+
dimColor: drilldownTab !== "spans",
|
|
1710
|
+
underline: drilldownTab === "spans",
|
|
1711
|
+
children: [
|
|
1712
|
+
"Spans (",
|
|
1713
|
+
drilldownSpans.length,
|
|
1714
|
+
")"
|
|
1715
|
+
]
|
|
1716
|
+
}
|
|
1717
|
+
),
|
|
1718
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1719
|
+
ink.Text,
|
|
1720
|
+
{
|
|
1721
|
+
color: drilldownTab === "logs" ? "yellow" : void 0,
|
|
1722
|
+
dimColor: drilldownTab !== "logs",
|
|
1723
|
+
underline: drilldownTab === "logs",
|
|
1724
|
+
children: [
|
|
1725
|
+
"Logs (",
|
|
1726
|
+
drilldownLogs.length,
|
|
1727
|
+
")"
|
|
1728
|
+
]
|
|
1729
|
+
}
|
|
1730
|
+
)
|
|
1731
|
+
] })
|
|
1732
|
+
] }),
|
|
1733
|
+
drilldownTraceId != null && drilldownTab === "timeline" && drilldownTimeline.map((item, i) => {
|
|
1734
|
+
const isSel = i === drilldownSelectedIndex;
|
|
1735
|
+
if (item.type === "span" && item.span) {
|
|
1736
|
+
const s = item.span;
|
|
1737
|
+
const node = drilldownTree.find(
|
|
1738
|
+
(n) => n.span.spanId === s.spanId
|
|
1739
|
+
);
|
|
1740
|
+
const depth = node?.depth ?? 0;
|
|
1741
|
+
const indent = " ".repeat(depth);
|
|
1742
|
+
const barLen = Math.max(
|
|
1743
|
+
1,
|
|
1744
|
+
Math.round(
|
|
1745
|
+
s.durationMs / Math.max(1, drilldownSummary?.durationMs ?? 1) * 10
|
|
1746
|
+
)
|
|
1747
|
+
);
|
|
1748
|
+
const bar = "\u2588".repeat(barLen);
|
|
1749
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1750
|
+
ink.Text,
|
|
1751
|
+
{
|
|
1752
|
+
backgroundColor: isSel ? "gray" : void 0,
|
|
1753
|
+
color: isSel ? "white" : void 0,
|
|
1754
|
+
children: [
|
|
1755
|
+
isSel ? "\u25B8" : " ",
|
|
1756
|
+
" ",
|
|
1757
|
+
indent,
|
|
1758
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "green", children: bar }),
|
|
1759
|
+
" ",
|
|
1760
|
+
s.name,
|
|
1761
|
+
" ",
|
|
1762
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1763
|
+
formatDurationMs(s.durationMs),
|
|
1764
|
+
s.kind ? ` ${s.kind}` : ""
|
|
1765
|
+
] })
|
|
1766
|
+
]
|
|
1767
|
+
}
|
|
1768
|
+
) }, `${s.spanId}-${i}`);
|
|
1769
|
+
} else if (item.type === "log" && item.log) {
|
|
1770
|
+
const l = item.log;
|
|
1771
|
+
const levelColor = l.level === "error" ? "red" : l.level === "warn" ? "yellow" : "blue";
|
|
1772
|
+
const relTime = drilldownSummary ? `+${formatDurationMs(l.time - (drilldownSummary.spans[0]?.startTime ?? l.time))}` : "";
|
|
1773
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1774
|
+
ink.Text,
|
|
1775
|
+
{
|
|
1776
|
+
backgroundColor: isSel ? "gray" : void 0,
|
|
1777
|
+
color: isSel ? "white" : void 0,
|
|
1778
|
+
children: [
|
|
1779
|
+
isSel ? "\u25B8" : " ",
|
|
1780
|
+
" ",
|
|
1781
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: levelColor, children: [
|
|
1782
|
+
"\u2139 ",
|
|
1783
|
+
l.level.toUpperCase()
|
|
1784
|
+
] }),
|
|
1785
|
+
" ",
|
|
1786
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: truncate(l.message, 40) }),
|
|
1787
|
+
" ",
|
|
1788
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: relTime })
|
|
1789
|
+
]
|
|
1790
|
+
}
|
|
1791
|
+
) }, `log-${i}`);
|
|
1792
|
+
}
|
|
1793
|
+
return null;
|
|
1794
|
+
}),
|
|
1795
|
+
drilldownTraceId != null && drilldownTab === "spans" && drilldownTree.map((node, i) => renderTreeRow(node, i)),
|
|
1796
|
+
drilldownTraceId != null && drilldownTab === "logs" && drilldownLogs.map((log, i) => {
|
|
1797
|
+
const isSel = i === drilldownSelectedIndex;
|
|
1798
|
+
const levelColor = log.level === "error" ? "red" : log.level === "warn" ? "yellow" : log.level === "info" ? "green" : void 0;
|
|
1799
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1800
|
+
ink.Text,
|
|
1801
|
+
{
|
|
1802
|
+
backgroundColor: isSel ? "gray" : void 0,
|
|
1803
|
+
color: isSel ? "white" : void 0,
|
|
1804
|
+
children: [
|
|
1805
|
+
isSel ? "\u25B8" : " ",
|
|
1806
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: levelColor, children: [
|
|
1807
|
+
" ",
|
|
1808
|
+
log.level.toUpperCase()
|
|
1809
|
+
] }),
|
|
1810
|
+
" ",
|
|
1811
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1812
|
+
"[",
|
|
1813
|
+
truncate(log.message, 50),
|
|
1814
|
+
"]"
|
|
1815
|
+
] })
|
|
1816
|
+
]
|
|
1817
|
+
}
|
|
1818
|
+
) }, `log-${i}`);
|
|
1819
|
+
}),
|
|
1820
|
+
drilldownTraceId == null && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: viewMode === "trace" ? filteredSummaries.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
|
|
996
1821
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No traces yet. Call a traced function or hit an endpoint to see them here." }),
|
|
997
1822
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Tip: trace() your handlers with autotel to get spans." })
|
|
998
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children:
|
|
1823
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: drilldownTraceId == null ? filteredSummaries.slice(0, 20).map((t2, i) => {
|
|
999
1824
|
const isSel = i === selected;
|
|
1000
1825
|
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
|
|
1001
1826
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
|
|
1002
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color:
|
|
1827
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: t2.hasError ? "red" : void 0, children: truncate(t2.rootName, 20) }),
|
|
1003
1828
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1004
1829
|
" ",
|
|
1005
|
-
formatDurationMs(
|
|
1830
|
+
formatDurationMs(t2.durationMs)
|
|
1006
1831
|
] }),
|
|
1007
1832
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1008
1833
|
" ",
|
|
1009
|
-
truncate(
|
|
1834
|
+
truncate(t2.traceId, 8)
|
|
1010
1835
|
] }),
|
|
1011
1836
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1012
1837
|
" ",
|
|
1013
|
-
formatRelative(
|
|
1838
|
+
formatRelative(t2.lastEndTime)
|
|
1014
1839
|
] })
|
|
1015
|
-
] },
|
|
1840
|
+
] }, t2.traceId);
|
|
1016
1841
|
}) : traceTree.slice(0, 20).map((node, i) => renderTreeRow(node, i)) }) : viewMode === "span" ? filteredSpans.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
|
|
1017
1842
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No spans yet. Call a traced function or hit an endpoint to see them here." }),
|
|
1018
1843
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Tip: trace() your handlers with autotel to get spans." })
|
|
1019
1844
|
] }) : filteredSpans.slice(0, 20).map((s, i) => {
|
|
1020
1845
|
const isSel = i === selected;
|
|
1021
1846
|
const statusColor = s.status === "ERROR" ? "red" : s.durationMs > 500 ? "yellow" : "green";
|
|
1022
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1847
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1848
|
+
ink.Box,
|
|
1849
|
+
{
|
|
1850
|
+
flexDirection: "row",
|
|
1851
|
+
children: [
|
|
1852
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
|
|
1853
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: colors ? statusColor : void 0, children: truncate(s.name, 26) }),
|
|
1854
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1855
|
+
" ",
|
|
1856
|
+
formatDurationMs(s.durationMs)
|
|
1857
|
+
] }),
|
|
1858
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1859
|
+
" ",
|
|
1860
|
+
formatRelative(s.endTime)
|
|
1861
|
+
] })
|
|
1862
|
+
]
|
|
1863
|
+
},
|
|
1864
|
+
`${s.spanId}-${s.startTime}`
|
|
1865
|
+
);
|
|
1034
1866
|
}) : 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
1867
|
const isSel = i === selected;
|
|
1036
1868
|
const errorRate = svc.total ? svc.errors / svc.total * 100 : 0;
|
|
@@ -1053,7 +1885,7 @@ ${json}
|
|
|
1053
1885
|
formatDurationMs(svc.p95Ms)
|
|
1054
1886
|
] })
|
|
1055
1887
|
] }, svc.serviceName);
|
|
1056
|
-
}) : viewMode === "errors" ?
|
|
1888
|
+
}) : viewMode === "errors" ? filteredErrorSummaries.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No errors yet." }) }) : filteredErrorSummaries.slice(0, 20).map((e, i) => {
|
|
1057
1889
|
const isSel = i === selected;
|
|
1058
1890
|
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
|
|
1059
1891
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
|
|
@@ -1088,11 +1920,11 @@ ${json}
|
|
|
1088
1920
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }),
|
|
1089
1921
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: truncate(log.message, 32) })
|
|
1090
1922
|
] }, `${log.time}-${i}`);
|
|
1091
|
-
})
|
|
1923
|
+
}) })
|
|
1092
1924
|
]
|
|
1093
1925
|
}
|
|
1094
1926
|
),
|
|
1095
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1927
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1096
1928
|
ink.Box,
|
|
1097
1929
|
{
|
|
1098
1930
|
flexDirection: "column",
|
|
@@ -1101,11 +1933,139 @@ ${json}
|
|
|
1101
1933
|
borderColor: "gray",
|
|
1102
1934
|
paddingX: 1,
|
|
1103
1935
|
paddingY: 0,
|
|
1104
|
-
children: [
|
|
1936
|
+
children: aiActive ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1937
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 1, justifyContent: "space-between", children: [
|
|
1938
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "AI Assistant" }),
|
|
1939
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: aiState.status === "streaming" ? "(streaming...)" : aiState.status === "unconfigured" ? "(no provider)" : aiState.status === "error" ? "(error)" : "" })
|
|
1940
|
+
] }),
|
|
1941
|
+
aiState.status === "unconfigured" ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
|
|
1942
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No AI provider configured." }),
|
|
1943
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Set AI_PROVIDER and AI_MODEL env vars, or start Ollama locally." }),
|
|
1944
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Press 'a' to close this panel." })
|
|
1945
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1946
|
+
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." }),
|
|
1947
|
+
aiMessages.slice(-10).map((msg, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1948
|
+
ink.Box,
|
|
1949
|
+
{
|
|
1950
|
+
flexDirection: "column",
|
|
1951
|
+
marginBottom: msg.role === "assistant" ? 1 : 0,
|
|
1952
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: msg.role === "user" ? "cyan" : void 0, children: [
|
|
1953
|
+
msg.role === "user" ? "> " : "",
|
|
1954
|
+
msg.content.slice(0, 500),
|
|
1955
|
+
msg.content.length > 500 ? "..." : ""
|
|
1956
|
+
] })
|
|
1957
|
+
},
|
|
1958
|
+
i
|
|
1959
|
+
)),
|
|
1960
|
+
aiState.status === "error" && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "red", children: [
|
|
1961
|
+
"Error: ",
|
|
1962
|
+
aiState.message
|
|
1963
|
+
] }),
|
|
1964
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1965
|
+
ink.Box,
|
|
1966
|
+
{
|
|
1967
|
+
marginTop: 1,
|
|
1968
|
+
borderStyle: "single",
|
|
1969
|
+
borderColor: "cyan",
|
|
1970
|
+
paddingX: 1,
|
|
1971
|
+
children: [
|
|
1972
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "cyan", children: "> " }),
|
|
1973
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: aiInput || (aiInputMode ? "(type your question)" : "(press a to focus)") })
|
|
1974
|
+
]
|
|
1975
|
+
}
|
|
1976
|
+
)
|
|
1977
|
+
] })
|
|
1978
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1105
1979
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginBottom: 1, children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Details" }) }),
|
|
1106
|
-
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1980
|
+
drilldownTraceId != null && drilldownSelectedItem?.type === "span" && drilldownSelectedItem.span ? (() => {
|
|
1981
|
+
const span = drilldownSelectedItem.span;
|
|
1982
|
+
const { key: keyAttrs, rest: restAttrs } = keyAttrsAndRest(
|
|
1983
|
+
span.attributes
|
|
1984
|
+
);
|
|
1985
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1986
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
1987
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Name: " }),
|
|
1988
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: span.name })
|
|
1989
|
+
] }),
|
|
1990
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
1991
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Status: " }),
|
|
1992
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: span.status === "ERROR" ? "red" : "green", children: span.status })
|
|
1993
|
+
] }),
|
|
1994
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
1995
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Duration: " }),
|
|
1996
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: formatDurationMs(span.durationMs) })
|
|
1997
|
+
] }),
|
|
1998
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1999
|
+
"Trace: ",
|
|
2000
|
+
span.traceId
|
|
2001
|
+
] }),
|
|
2002
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2003
|
+
"Span: ",
|
|
2004
|
+
span.spanId
|
|
2005
|
+
] }),
|
|
2006
|
+
span.parentSpanId && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2007
|
+
"Parent: ",
|
|
2008
|
+
span.parentSpanId
|
|
2009
|
+
] }),
|
|
2010
|
+
span.kind && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2011
|
+
"Kind: ",
|
|
2012
|
+
span.kind
|
|
2013
|
+
] }),
|
|
2014
|
+
keyAttrs.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 1, flexDirection: "column", children: [
|
|
2015
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Key attributes" }),
|
|
2016
|
+
keyAttrs.slice(0, 6).map(([k, v]) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2017
|
+
truncate(k, 18),
|
|
2018
|
+
": ",
|
|
2019
|
+
truncate(String(v), 28)
|
|
2020
|
+
] }, k))
|
|
2021
|
+
] }),
|
|
2022
|
+
restAttrs.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 1, flexDirection: "column", children: [
|
|
2023
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Attributes" }),
|
|
2024
|
+
restAttrs.slice(0, 8).map(([k, v]) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2025
|
+
truncate(k, 18),
|
|
2026
|
+
": ",
|
|
2027
|
+
truncate(String(v), 28)
|
|
2028
|
+
] }, k))
|
|
2029
|
+
] }),
|
|
2030
|
+
keyAttrs.length === 0 && restAttrs.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "(no attributes)" })
|
|
2031
|
+
] });
|
|
2032
|
+
})() : drilldownTraceId != null && drilldownSelectedItem?.type === "log" && drilldownSelectedItem.log ? (() => {
|
|
2033
|
+
const log = drilldownSelectedItem.log;
|
|
2034
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2035
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
2036
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Level: " }),
|
|
2037
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: log.level.toUpperCase() })
|
|
2038
|
+
] }),
|
|
2039
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
2040
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Time: " }),
|
|
2041
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: new Date(log.time).toISOString() })
|
|
2042
|
+
] }),
|
|
2043
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
2044
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Message: " }),
|
|
2045
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: log.message })
|
|
2046
|
+
] }),
|
|
2047
|
+
log.traceId && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2048
|
+
"Trace: ",
|
|
2049
|
+
log.traceId
|
|
2050
|
+
] }),
|
|
2051
|
+
log.spanId && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2052
|
+
"Span: ",
|
|
2053
|
+
log.spanId
|
|
2054
|
+
] }),
|
|
2055
|
+
log.attributes && Object.keys(log.attributes).length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 1, flexDirection: "column", children: [
|
|
2056
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Attributes" }),
|
|
2057
|
+
Object.entries(log.attributes).slice(0, 10).map(([k, v]) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
2058
|
+
truncate(k, 18),
|
|
2059
|
+
": ",
|
|
2060
|
+
truncate(String(v), 40)
|
|
2061
|
+
] }, k))
|
|
2062
|
+
] })
|
|
2063
|
+
] });
|
|
2064
|
+
})() : drilldownTraceId == null ? null : /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Select an item to view details." }),
|
|
2065
|
+
drilldownTraceId == null && (viewMode === "errors" ? (() => {
|
|
2066
|
+
const e = filteredErrorSummaries[selected] ?? null;
|
|
2067
|
+
if (!e)
|
|
2068
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Select an error to view details." });
|
|
1109
2069
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1110
2070
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
1111
2071
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Trace: " }),
|
|
@@ -1121,11 +2081,12 @@ ${json}
|
|
|
1121
2081
|
"Errors: ",
|
|
1122
2082
|
e.errorCount
|
|
1123
2083
|
] }),
|
|
1124
|
-
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "
|
|
2084
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Press T to jump to trace view for this trace." })
|
|
1125
2085
|
] });
|
|
1126
2086
|
})() : viewMode === "service-summary" ? (() => {
|
|
1127
2087
|
const svc = serviceStats[selected] ?? null;
|
|
1128
|
-
if (!svc)
|
|
2088
|
+
if (!svc)
|
|
2089
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Select a service to view details." });
|
|
1129
2090
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1130
2091
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
1131
2092
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Service: " }),
|
|
@@ -1139,7 +2100,8 @@ ${json}
|
|
|
1139
2100
|
" | Avg:",
|
|
1140
2101
|
" ",
|
|
1141
2102
|
formatDurationMs(svc.avgMs),
|
|
1142
|
-
" | P95:
|
|
2103
|
+
" | P95:",
|
|
2104
|
+
" ",
|
|
1143
2105
|
formatDurationMs(svc.p95Ms)
|
|
1144
2106
|
] }),
|
|
1145
2107
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 1, flexDirection: "column", children: [
|
|
@@ -1159,7 +2121,8 @@ ${json}
|
|
|
1159
2121
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Hot spans" }),
|
|
1160
2122
|
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
2123
|
truncate(h.name, 20),
|
|
1162
|
-
" p95
|
|
2124
|
+
" p95",
|
|
2125
|
+
" ",
|
|
1163
2126
|
formatDurationMs(h.p95Ms),
|
|
1164
2127
|
" (",
|
|
1165
2128
|
h.count,
|
|
@@ -1209,7 +2172,8 @@ ${json}
|
|
|
1209
2172
|
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
|
|
1210
2173
|
"+",
|
|
1211
2174
|
relMs,
|
|
1212
|
-
"ms span
|
|
2175
|
+
"ms span",
|
|
2176
|
+
" ",
|
|
1213
2177
|
truncate(item.span.name, 20)
|
|
1214
2178
|
] }, `span-${idx}`);
|
|
1215
2179
|
}
|
|
@@ -1217,7 +2181,8 @@ ${json}
|
|
|
1217
2181
|
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
1218
2182
|
"+",
|
|
1219
2183
|
relMs,
|
|
1220
|
-
"ms log
|
|
2184
|
+
"ms log",
|
|
2185
|
+
" ",
|
|
1221
2186
|
truncate(item.log.message, 24)
|
|
1222
2187
|
] }, `log-${idx}`);
|
|
1223
2188
|
}
|
|
@@ -1232,16 +2197,25 @@ ${json}
|
|
|
1232
2197
|
] }),
|
|
1233
2198
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
1234
2199
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Status: " }),
|
|
1235
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2200
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2201
|
+
ink.Text,
|
|
2202
|
+
{
|
|
2203
|
+
color: currentSpan.status === "ERROR" ? "red" : "green",
|
|
2204
|
+
children: currentSpan.status
|
|
2205
|
+
}
|
|
2206
|
+
)
|
|
1236
2207
|
] }),
|
|
1237
2208
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
1238
2209
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Duration: " }),
|
|
1239
2210
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
|
|
1240
2211
|
formatDurationMs(currentSpan.durationMs),
|
|
1241
2212
|
perSpanNameStats.byName.has(currentSpan.name) && (() => {
|
|
1242
|
-
const p = perSpanNameStats.byName.get(
|
|
2213
|
+
const p = perSpanNameStats.byName.get(
|
|
2214
|
+
currentSpan.name
|
|
2215
|
+
);
|
|
1243
2216
|
const ratio = p.avgMs > 0 ? currentSpan.durationMs / p.avgMs : 1;
|
|
1244
|
-
if (ratio >= 1.5)
|
|
2217
|
+
if (ratio >= 1.5)
|
|
2218
|
+
return ` (${ratio.toFixed(1)}x avg)`;
|
|
1245
2219
|
return "";
|
|
1246
2220
|
})()
|
|
1247
2221
|
] })
|
|
@@ -1284,10 +2258,12 @@ ${json}
|
|
|
1284
2258
|
keyAttrs.length === 0 && restAttrs.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "(no attributes)" })
|
|
1285
2259
|
] });
|
|
1286
2260
|
})(),
|
|
1287
|
-
waterfallSpans.length > 0 &&
|
|
2261
|
+
waterfallSpans.length > 0 && drilldownTraceId != null && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 1, flexDirection: "column", children: [
|
|
1288
2262
|
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Waterfall" }),
|
|
1289
2263
|
waterfallSpans.slice(0, 10).map((w) => {
|
|
1290
|
-
const barLen = Math.round(
|
|
2264
|
+
const barLen = Math.round(
|
|
2265
|
+
w.span.durationMs / waterfallMaxMs * barWidth
|
|
2266
|
+
) || 1;
|
|
1291
2267
|
const bar = "\u2588".repeat(barLen);
|
|
1292
2268
|
const indent = " ".repeat(w.depth);
|
|
1293
2269
|
return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
|
|
@@ -1304,8 +2280,8 @@ ${json}
|
|
|
1304
2280
|
] }, w.span.spanId);
|
|
1305
2281
|
})
|
|
1306
2282
|
] })
|
|
1307
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Select a trace or span to view details." })
|
|
1308
|
-
]
|
|
2283
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Select a trace or span to view details." }))
|
|
2284
|
+
] })
|
|
1309
2285
|
}
|
|
1310
2286
|
)
|
|
1311
2287
|
] }),
|
|
@@ -1314,11 +2290,13 @@ ${json}
|
|
|
1314
2290
|
stats.total,
|
|
1315
2291
|
" | Span errors: ",
|
|
1316
2292
|
stats.errors,
|
|
1317
|
-
" | Logs:
|
|
2293
|
+
" | Logs:",
|
|
2294
|
+
" ",
|
|
1318
2295
|
logStats.total,
|
|
1319
2296
|
" | Log errors: ",
|
|
1320
2297
|
logStats.errors,
|
|
1321
|
-
" | Avg:
|
|
2298
|
+
" | Avg:",
|
|
2299
|
+
" ",
|
|
1322
2300
|
formatDurationMs(stats.avg),
|
|
1323
2301
|
" | P95: ",
|
|
1324
2302
|
formatDurationMs(stats.p95)
|
|
@@ -1336,6 +2314,7 @@ function renderTerminal(options = {}, stream) {
|
|
|
1336
2314
|
const showStats = options.showStats !== false;
|
|
1337
2315
|
const maxSpans = options.maxSpans ?? 100;
|
|
1338
2316
|
const colors = options.colors ?? Boolean(process.stdout.isTTY);
|
|
2317
|
+
const aiConfig = options.ai;
|
|
1339
2318
|
const stdinOption = process.stdin.isTTY ? process.stdin : void 0;
|
|
1340
2319
|
if (stream) {
|
|
1341
2320
|
try {
|
|
@@ -1348,7 +2327,8 @@ function renderTerminal(options = {}, stream) {
|
|
|
1348
2327
|
maxSpans,
|
|
1349
2328
|
colors,
|
|
1350
2329
|
stream,
|
|
1351
|
-
logStream: getTerminalLogStream()
|
|
2330
|
+
logStream: getTerminalLogStream(),
|
|
2331
|
+
aiConfig
|
|
1352
2332
|
}
|
|
1353
2333
|
),
|
|
1354
2334
|
{ stdin: stdinOption }
|
|
@@ -1464,7 +2444,8 @@ function toMs(unixNano) {
|
|
|
1464
2444
|
}
|
|
1465
2445
|
function mapStatus2(code) {
|
|
1466
2446
|
const normalized = typeof code === "string" ? code.toUpperCase() : code;
|
|
1467
|
-
if (normalized === 1 || normalized === "STATUS_CODE_OK" || normalized === "OK")
|
|
2447
|
+
if (normalized === 1 || normalized === "STATUS_CODE_OK" || normalized === "OK")
|
|
2448
|
+
return "OK";
|
|
1468
2449
|
if (normalized === 2 || normalized === "STATUS_CODE_ERROR" || normalized === "ERROR") {
|
|
1469
2450
|
return "ERROR";
|
|
1470
2451
|
}
|
|
@@ -1635,33 +2616,39 @@ function countOtlpMetrics(payload) {
|
|
|
1635
2616
|
|
|
1636
2617
|
// src/cli.ts
|
|
1637
2618
|
function printHelp() {
|
|
1638
|
-
process.stdout.write(
|
|
2619
|
+
process.stdout.write(
|
|
2620
|
+
String.raw`autotel-terminal - Standalone OTLP receiver with terminal dashboard
|
|
1639
2621
|
|
|
1640
2622
|
Usage: autotel-terminal [options]
|
|
1641
2623
|
|
|
1642
2624
|
Options:
|
|
1643
|
-
-p, --port <port>
|
|
1644
|
-
-H, --host <host>
|
|
1645
|
-
-t, --title <title>
|
|
1646
|
-
-h, --help
|
|
1647
|
-
-v, --version
|
|
2625
|
+
-p, --port <port> Port to listen on (default: 4319, env: AUTOTEL_TERMINAL_PORT)
|
|
2626
|
+
-H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_TERMINAL_HOST)
|
|
2627
|
+
-t, --title <title> Dashboard title (env: AUTOTEL_TERMINAL_TITLE)
|
|
2628
|
+
-h, --help Show this help message
|
|
2629
|
+
-v, --version Show version number
|
|
2630
|
+
|
|
2631
|
+
AI Options:
|
|
2632
|
+
--ai-provider <provider> AI provider: ollama, openai, openai-compatible (env: AI_PROVIDER)
|
|
2633
|
+
--ai-model <model> AI model name (env: AI_MODEL)
|
|
2634
|
+
--ai-api-key <key> API key for cloud providers (env: AI_API_KEY)
|
|
2635
|
+
--ai-base-url <url> Custom AI endpoint URL (env: AI_BASE_URL)
|
|
2636
|
+
|
|
2637
|
+
Auto-detection: if Ollama is running locally, it is used automatically.
|
|
2638
|
+
If OPENAI_API_KEY is set, OpenAI is used. Press 'a' in the dashboard to toggle AI.
|
|
1648
2639
|
|
|
1649
2640
|
Endpoints:
|
|
1650
|
-
POST /v1/traces
|
|
1651
|
-
POST /v1/logs
|
|
1652
|
-
POST /v1/metrics
|
|
1653
|
-
GET /healthz
|
|
2641
|
+
POST /v1/traces Receive OTLP JSON trace data
|
|
2642
|
+
POST /v1/logs Receive OTLP JSON log data
|
|
2643
|
+
POST /v1/metrics Receive OTLP JSON metric data (accepted and counted)
|
|
2644
|
+
GET /healthz Health check
|
|
1654
2645
|
|
|
1655
2646
|
Examples:
|
|
1656
2647
|
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");
|
|
2648
|
+
npx autotel-terminal --ai-provider ollama --ai-model granite4
|
|
2649
|
+
AI_API_KEY=sk-... npx autotel-terminal --ai-provider openai --ai-model gpt-4o
|
|
2650
|
+
` + "\n"
|
|
2651
|
+
);
|
|
1665
2652
|
}
|
|
1666
2653
|
function printVersion() {
|
|
1667
2654
|
try {
|
|
@@ -1678,7 +2665,8 @@ function parseArgs(argv) {
|
|
|
1678
2665
|
const options = {
|
|
1679
2666
|
port: Number(process.env.AUTOTEL_TERMINAL_PORT || 4319),
|
|
1680
2667
|
host: process.env.AUTOTEL_TERMINAL_HOST || "127.0.0.1",
|
|
1681
|
-
title: process.env.AUTOTEL_TERMINAL_TITLE
|
|
2668
|
+
title: process.env.AUTOTEL_TERMINAL_TITLE,
|
|
2669
|
+
ai: {}
|
|
1682
2670
|
};
|
|
1683
2671
|
for (let i = 0; i < argv.length; i++) {
|
|
1684
2672
|
const arg = argv[i];
|
|
@@ -1704,6 +2692,26 @@ function parseArgs(argv) {
|
|
|
1704
2692
|
if ((arg === "--title" || arg === "-t") && next) {
|
|
1705
2693
|
options.title = next;
|
|
1706
2694
|
i++;
|
|
2695
|
+
continue;
|
|
2696
|
+
}
|
|
2697
|
+
if (arg === "--ai-provider" && next) {
|
|
2698
|
+
options.ai.provider = next;
|
|
2699
|
+
i++;
|
|
2700
|
+
continue;
|
|
2701
|
+
}
|
|
2702
|
+
if (arg === "--ai-model" && next) {
|
|
2703
|
+
options.ai.model = next;
|
|
2704
|
+
i++;
|
|
2705
|
+
continue;
|
|
2706
|
+
}
|
|
2707
|
+
if (arg === "--ai-api-key" && next) {
|
|
2708
|
+
options.ai.apiKey = next;
|
|
2709
|
+
i++;
|
|
2710
|
+
continue;
|
|
2711
|
+
}
|
|
2712
|
+
if (arg === "--ai-base-url" && next) {
|
|
2713
|
+
options.ai.baseUrl = next;
|
|
2714
|
+
i++;
|
|
1707
2715
|
}
|
|
1708
2716
|
}
|
|
1709
2717
|
return options;
|
|
@@ -1718,7 +2726,8 @@ async function main() {
|
|
|
1718
2726
|
const logStream = getTerminalLogStream();
|
|
1719
2727
|
renderTerminal(
|
|
1720
2728
|
{
|
|
1721
|
-
title: options.title || `Autotel Terminal (${options.host}:${options.port})
|
|
2729
|
+
title: options.title || `Autotel Terminal (${options.host}:${options.port})`,
|
|
2730
|
+
ai: options.ai
|
|
1722
2731
|
},
|
|
1723
2732
|
spanStream
|
|
1724
2733
|
);
|