Skip to content

Commit 670ae28

Browse files
committed
feat(traces): Add traces UI
Signed-off-by: Richard Palethorpe <[email protected]>
1 parent cfef652 commit 670ae28

File tree

2 files changed

+204
-0
lines changed

2 files changed

+204
-0
lines changed

core/http/routes/ui.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,4 +317,24 @@ func RegisterUIRoutes(app *echo.Echo,
317317
// Render index
318318
return c.Render(200, "views/tts", summary)
319319
})
320+
321+
// Traces UI
322+
app.GET("/traces", func(c echo.Context) error {
323+
summary := map[string]interface{}{
324+
"Title": "LocalAI - Traces",
325+
"BaseURL": middleware.BaseURL(c),
326+
"Version": internal.PrintableVersion(),
327+
}
328+
return c.Render(200, "views/traces", summary)
329+
})
330+
331+
app.GET("/api/traces", func(c echo.Context) error {
332+
return c.JSON(200, middleware.GetAPILogs())
333+
})
334+
335+
app.POST("/api/traces/clear", func(c echo.Context) error {
336+
middleware.ClearAPILogs()
337+
return c.NoContent(204)
338+
})
339+
320340
}

core/http/views/traces.html

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
{{template "views/partials/head" .}}
4+
5+
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
6+
<div class="flex flex-col min-h-screen" x-data="tracesApp()" x-init="init()">
7+
8+
{{template "views/partials/navbar" .}}
9+
10+
<div class="container mx-auto px-4 py-8 flex-grow">
11+
12+
<!-- Hero Header -->
13+
<div class="hero-section">
14+
<div class="hero-content">
15+
<h1 class="hero-title">
16+
API Traces
17+
</h1>
18+
<p class="hero-subtitle">View logged API requests and responses</p>
19+
<div class="flex flex-wrap justify-center gap-3">
20+
<button @click="clearTraces()" class="btn-secondary text-sm py-1.5 px-3">
21+
<i class="fas fa-trash mr-1.5 text-[10px]"></i>
22+
<span>Clear Traces</span>
23+
</button>
24+
</div>
25+
</div>
26+
</div>
27+
28+
<!-- Traces Table -->
29+
<div class="mt-8">
30+
<div class="overflow-x-auto">
31+
<table class="w-full border-collapse">
32+
<thead>
33+
<tr class="border-b border-[var(--color-bg-secondary)]">
34+
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Method</th>
35+
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Path</th>
36+
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Status</th>
37+
<th class="text-right p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Actions</th>
38+
</tr>
39+
</thead>
40+
<tbody>
41+
<template x-for="(trace, index) in traces" :key="index">
42+
<tr class="hover:bg-[var(--color-bg-secondary)]/50 border-b border-[var(--color-bg-secondary)] transition-colors">
43+
<td class="p-2" x-text="trace.request.method"></td>
44+
<td class="p-2" x-text="trace.request.path"></td>
45+
<td class="p-2" x-text="trace.response.status"></td>
46+
<td class="p-2 text-right">
47+
<button @click="showDetails(index)" class="text-[var(--color-primary)]/60 hover:text-[var(--color-primary)] hover:bg-[var(--color-primary)]/10 rounded p-1 transition-colors">
48+
<i class="fas fa-eye text-xs"></i>
49+
</button>
50+
</td>
51+
</tr>
52+
</template>
53+
</tbody>
54+
</table>
55+
</div>
56+
</div>
57+
58+
<!-- Details Modal -->
59+
<div x-show="selectedTrace !== null" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" @click="selectedTrace = null">
60+
<div class="bg-[var(--color-bg-secondary)] rounded-lg p-6 max-w-4xl w-full max-h-[90vh] overflow-auto" @click.stop>
61+
<div class="flex justify-between mb-4">
62+
<h2 class="h3">Trace Details</h2>
63+
<button @click="selectedTrace = null" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">
64+
<i class="fas fa-times"></i>
65+
</button>
66+
</div>
67+
<div class="grid grid-cols-2 gap-4">
68+
<div>
69+
<h3 class="text-lg font-semibold mb-2">Request Body</h3>
70+
<div id="requestEditor" class="h-96 border border-[var(--color-primary-border)]/20"></div>
71+
</div>
72+
<div>
73+
<h3 class="text-lg font-semibold mb-2">Response Body</h3>
74+
<div id="responseEditor" class="h-96 border border-[var(--color-primary-border)]/20"></div>
75+
</div>
76+
</div>
77+
</div>
78+
</div>
79+
80+
</div>
81+
82+
{{template "views/partials/footer" .}}
83+
84+
</div>
85+
86+
<!-- CodeMirror -->
87+
<link rel="stylesheet" href="static/assets/codemirror.min.css">
88+
<script src="static/assets/codemirror.min.js"></script>
89+
<script src="static/assets/javascript.min.js"></script>
90+
91+
<!-- Styles from model-editor -->
92+
<style>
93+
.CodeMirror {
94+
height: 100% !important;
95+
font-family: monospace;
96+
}
97+
</style>
98+
99+
<script>
100+
function tracesApp() {
101+
return {
102+
traces: [],
103+
selectedTrace: null,
104+
requestEditor: null,
105+
responseEditor: null,
106+
107+
init() {
108+
this.fetchTraces();
109+
setInterval(() => this.fetchTraces(), 5000);
110+
},
111+
112+
async fetchTraces() {
113+
const response = await fetch('/api/traces');
114+
this.traces = await response.json();
115+
},
116+
117+
async clearTraces() {
118+
if (confirm('Clear all traces?')) {
119+
await fetch('/api/traces/clear', { method: 'POST' });
120+
this.traces = [];
121+
}
122+
},
123+
124+
showDetails(index) {
125+
this.selectedTrace = index;
126+
this.$nextTick(() => {
127+
const trace = this.traces[index];
128+
129+
const decodeBase64 = (base64) => {
130+
const binaryString = atob(base64);
131+
const bytes = new Uint8Array(binaryString.length);
132+
for (let i = 0; i < binaryString.length; i++) {
133+
bytes[i] = binaryString.charCodeAt(i);
134+
}
135+
return new TextDecoder().decode(bytes);
136+
};
137+
138+
const formatBody = (bodyText) => {
139+
try {
140+
const json = JSON.parse(bodyText);
141+
return JSON.stringify(json, null, 2);
142+
} catch {
143+
return bodyText;
144+
}
145+
};
146+
147+
const reqBody = formatBody(decodeBase64(trace.request.body));
148+
const resBody = formatBody(decodeBase64(trace.response.body));
149+
150+
if (!this.requestEditor) {
151+
this.requestEditor = CodeMirror(document.getElementById('requestEditor'), {
152+
value: reqBody,
153+
mode: 'javascript',
154+
json: true,
155+
theme: 'default',
156+
lineNumbers: true,
157+
readOnly: true,
158+
lineWrapping: true
159+
});
160+
} else {
161+
this.requestEditor.setValue(reqBody);
162+
}
163+
164+
if (!this.responseEditor) {
165+
this.responseEditor = CodeMirror(document.getElementById('responseEditor'), {
166+
value: resBody,
167+
mode: 'javascript',
168+
json: true,
169+
theme: 'default',
170+
lineNumbers: true,
171+
readOnly: true,
172+
lineWrapping: true
173+
});
174+
} else {
175+
this.responseEditor.setValue(resBody);
176+
}
177+
});
178+
}
179+
}
180+
}
181+
</script>
182+
183+
</body>
184+
</html>

0 commit comments

Comments
 (0)