@syndew/oluso-plugin-telemetry 0.0.0-stage → 1.30.1
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.
|
@@ -0,0 +1,2548 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, Injectable, signal, computed, Component } from '@angular/core';
|
|
3
|
+
import { ApiService } from '@syndew/oluso-admin-ui';
|
|
4
|
+
import * as i1$1 from '@angular/common';
|
|
5
|
+
import { CommonModule } from '@angular/common';
|
|
6
|
+
import * as i1 from '@angular/forms';
|
|
7
|
+
import { FormsModule } from '@angular/forms';
|
|
8
|
+
import { RouterLink, Router, ActivatedRoute } from '@angular/router';
|
|
9
|
+
import { IconComponent } from '@syndew/oluso-ui-core';
|
|
10
|
+
import { Subject, interval, takeUntil, forkJoin } from 'rxjs';
|
|
11
|
+
|
|
12
|
+
const TELEMETRY_ROUTES = [
|
|
13
|
+
{
|
|
14
|
+
path: '',
|
|
15
|
+
pathMatch: 'full',
|
|
16
|
+
redirectTo: 'dashboard',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
path: 'dashboard',
|
|
20
|
+
loadComponent: () => Promise.resolve().then(function () { return telemetryDashboard_component; }).then((m) => m.TelemetryDashboardComponent),
|
|
21
|
+
data: { title: 'Telemetry Dashboard' },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
path: 'logs',
|
|
25
|
+
loadComponent: () => Promise.resolve().then(function () { return telemetryLogs_component; }).then((m) => m.TelemetryLogsComponent),
|
|
26
|
+
data: { title: 'Application Logs' },
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
path: 'traces',
|
|
30
|
+
loadComponent: () => Promise.resolve().then(function () { return telemetryTraces_component; }).then((m) => m.TelemetryTracesComponent),
|
|
31
|
+
data: { title: 'Distributed Traces' },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
path: 'traces/:traceId',
|
|
35
|
+
loadComponent: () => Promise.resolve().then(function () { return telemetryTraces_component; }).then((m) => m.TelemetryTracesComponent),
|
|
36
|
+
data: { title: 'Trace Details' },
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Telemetry Admin Plugin
|
|
42
|
+
*
|
|
43
|
+
* Provides system monitoring and observability functionality:
|
|
44
|
+
* - Dashboard with overview metrics
|
|
45
|
+
* - Application logs viewer with filtering and live tail
|
|
46
|
+
* - Distributed traces explorer
|
|
47
|
+
*/
|
|
48
|
+
const TELEMETRY_PLUGIN = {
|
|
49
|
+
name: 'oluso-telemetry',
|
|
50
|
+
version: '1.0.0',
|
|
51
|
+
// Plugin routes - will be automatically added under /telemetry path
|
|
52
|
+
routes: [
|
|
53
|
+
{
|
|
54
|
+
path: 'telemetry',
|
|
55
|
+
children: TELEMETRY_ROUTES,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
navItems: [
|
|
59
|
+
{
|
|
60
|
+
id: 'telemetry',
|
|
61
|
+
label: 'Telemetry',
|
|
62
|
+
path: '/telemetry',
|
|
63
|
+
icon: 'chart-bar',
|
|
64
|
+
order: 76,
|
|
65
|
+
superAdminOnly: true,
|
|
66
|
+
children: [
|
|
67
|
+
{ label: 'Dashboard', path: '/telemetry', icon: 'chart-bar', order: 0 },
|
|
68
|
+
{ label: 'Logs', path: '/telemetry/logs', icon: 'document-text', order: 1 },
|
|
69
|
+
{ label: 'Traces', path: '/telemetry/traces', icon: 'queue-list', order: 2 },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Helper function to get Telemetry navigation items
|
|
76
|
+
*/
|
|
77
|
+
function getTelemetryNavItems() {
|
|
78
|
+
return TELEMETRY_PLUGIN.navItems ?? [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get interval based on time range
|
|
83
|
+
*/
|
|
84
|
+
function getIntervalForRange(range) {
|
|
85
|
+
switch (range) {
|
|
86
|
+
case '1h':
|
|
87
|
+
return '5m';
|
|
88
|
+
case '24h':
|
|
89
|
+
return '1h';
|
|
90
|
+
case '7d':
|
|
91
|
+
return '6h';
|
|
92
|
+
case '30d':
|
|
93
|
+
return '1d';
|
|
94
|
+
default:
|
|
95
|
+
return '1h';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Format duration in ms to human readable string
|
|
100
|
+
*/
|
|
101
|
+
function formatDuration(ms) {
|
|
102
|
+
if (ms < 1)
|
|
103
|
+
return `${(ms * 1000).toFixed(0)}us`;
|
|
104
|
+
if (ms < 1000)
|
|
105
|
+
return `${ms.toFixed(0)}ms`;
|
|
106
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Log level display configuration (designed for dark terminal-style background)
|
|
110
|
+
*/
|
|
111
|
+
const LOG_LEVEL_CONFIG = {
|
|
112
|
+
trace: { textColor: 'text-gray-300', bgColor: 'bg-gray-700', label: 'TRACE' },
|
|
113
|
+
debug: { textColor: 'text-gray-300', bgColor: 'bg-gray-600', label: 'DEBUG' },
|
|
114
|
+
info: { textColor: 'text-blue-200', bgColor: 'bg-blue-900', label: 'INFO' },
|
|
115
|
+
warn: { textColor: 'text-yellow-200', bgColor: 'bg-yellow-900', label: 'WARN' },
|
|
116
|
+
error: { textColor: 'text-red-200', bgColor: 'bg-red-900', label: 'ERROR' },
|
|
117
|
+
fatal: { textColor: 'text-red-100', bgColor: 'bg-red-800', label: 'FATAL' },
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Trace status display configuration
|
|
121
|
+
*/
|
|
122
|
+
const TRACE_STATUS_CONFIG = {
|
|
123
|
+
ok: { textColor: 'text-green-500', bgColor: 'bg-green-100' },
|
|
124
|
+
error: { textColor: 'text-red-500', bgColor: 'bg-red-100' },
|
|
125
|
+
unset: { textColor: 'text-gray-400', bgColor: 'bg-gray-100' },
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
class TelemetryService {
|
|
129
|
+
api = inject(ApiService);
|
|
130
|
+
// ============================================
|
|
131
|
+
// Dashboard Metrics
|
|
132
|
+
// ============================================
|
|
133
|
+
/**
|
|
134
|
+
* Get overview metrics for the telemetry dashboard
|
|
135
|
+
*/
|
|
136
|
+
getOverviewMetrics(timeRange = '24h') {
|
|
137
|
+
return this.api.get('/telemetry/metrics/overview', { timeRange });
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get time series data for a specific metric
|
|
141
|
+
*/
|
|
142
|
+
getMetricSeries(metricName, timeRange = '24h') {
|
|
143
|
+
const interval = getIntervalForRange(timeRange);
|
|
144
|
+
return this.api.get(`/telemetry/metrics/${metricName}`, {
|
|
145
|
+
timeRange,
|
|
146
|
+
interval,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// ============================================
|
|
150
|
+
// Logs
|
|
151
|
+
// ============================================
|
|
152
|
+
/**
|
|
153
|
+
* Get application logs with filtering and pagination
|
|
154
|
+
*/
|
|
155
|
+
getLogs(query) {
|
|
156
|
+
return this.api.get('/telemetry/logs', this.cleanQuery(query));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Get available log categories for filtering
|
|
160
|
+
*/
|
|
161
|
+
getLogCategories() {
|
|
162
|
+
return this.api.get('/telemetry/logs/categories');
|
|
163
|
+
}
|
|
164
|
+
// ============================================
|
|
165
|
+
// Traces
|
|
166
|
+
// ============================================
|
|
167
|
+
/**
|
|
168
|
+
* Get distributed traces with filtering and pagination
|
|
169
|
+
*/
|
|
170
|
+
getTraces(query) {
|
|
171
|
+
return this.api.get('/telemetry/traces', this.cleanQuery(query));
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Get details of a specific trace by ID
|
|
175
|
+
*/
|
|
176
|
+
getTrace(traceId) {
|
|
177
|
+
return this.api.get(`/telemetry/traces/${traceId}`);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Trace backend configuration status. Used by the Traces page to render
|
|
181
|
+
* a "tracing not configured" notice in environments without Tempo.
|
|
182
|
+
*/
|
|
183
|
+
getTraceBackendStatus() {
|
|
184
|
+
return this.api.get('/telemetry/traces/status');
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Log backend configuration status. Returns the source the Logs page is
|
|
188
|
+
* actually reading from ("database", "loki", "none") so the UI can
|
|
189
|
+
* surface a small badge and degrade gracefully when nothing is wired.
|
|
190
|
+
*/
|
|
191
|
+
getLogBackendStatus() {
|
|
192
|
+
return this.api.get('/telemetry/logs/status');
|
|
193
|
+
}
|
|
194
|
+
// ============================================
|
|
195
|
+
// Helper Methods
|
|
196
|
+
// ============================================
|
|
197
|
+
/**
|
|
198
|
+
* Clean query object by removing undefined values
|
|
199
|
+
*/
|
|
200
|
+
cleanQuery(query) {
|
|
201
|
+
const cleaned = {};
|
|
202
|
+
for (const [key, value] of Object.entries(query)) {
|
|
203
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
204
|
+
cleaned[key] = value;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return cleaned;
|
|
208
|
+
}
|
|
209
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: TelemetryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
210
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: TelemetryService, providedIn: 'root' });
|
|
211
|
+
}
|
|
212
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: TelemetryService, decorators: [{
|
|
213
|
+
type: Injectable,
|
|
214
|
+
args: [{
|
|
215
|
+
providedIn: 'root',
|
|
216
|
+
}]
|
|
217
|
+
}] });
|
|
218
|
+
|
|
219
|
+
class TelemetryDashboardComponent {
|
|
220
|
+
telemetryService = inject(TelemetryService);
|
|
221
|
+
destroy$ = new Subject();
|
|
222
|
+
metrics = signal(null, ...(ngDevMode ? [{ debugName: "metrics" }] : /* istanbul ignore next */ []));
|
|
223
|
+
tokenSeries = signal(null, ...(ngDevMode ? [{ debugName: "tokenSeries" }] : /* istanbul ignore next */ []));
|
|
224
|
+
authSeries = signal(null, ...(ngDevMode ? [{ debugName: "authSeries" }] : /* istanbul ignore next */ []));
|
|
225
|
+
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
226
|
+
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
227
|
+
timeRange = signal('24h', ...(ngDevMode ? [{ debugName: "timeRange" }] : /* istanbul ignore next */ []));
|
|
228
|
+
autoRefresh = signal(false, ...(ngDevMode ? [{ debugName: "autoRefresh" }] : /* istanbul ignore next */ []));
|
|
229
|
+
systemHealth = computed(() => {
|
|
230
|
+
const m = this.metrics();
|
|
231
|
+
if (!m)
|
|
232
|
+
return '-';
|
|
233
|
+
return m.authSuccessRate >= 95 && m.avgRequestDuration < 500 ? 'Healthy' : 'Degraded';
|
|
234
|
+
}, ...(ngDevMode ? [{ debugName: "systemHealth" }] : /* istanbul ignore next */ []));
|
|
235
|
+
ngOnInit() {
|
|
236
|
+
this.loadData();
|
|
237
|
+
}
|
|
238
|
+
ngOnDestroy() {
|
|
239
|
+
this.destroy$.next();
|
|
240
|
+
this.destroy$.complete();
|
|
241
|
+
}
|
|
242
|
+
setTimeRange(range) {
|
|
243
|
+
this.timeRange.set(range);
|
|
244
|
+
this.loadData();
|
|
245
|
+
}
|
|
246
|
+
toggleAutoRefresh() {
|
|
247
|
+
this.autoRefresh.update((v) => !v);
|
|
248
|
+
if (this.autoRefresh()) {
|
|
249
|
+
interval(30000)
|
|
250
|
+
.pipe(takeUntil(this.destroy$))
|
|
251
|
+
.subscribe(() => {
|
|
252
|
+
if (this.autoRefresh()) {
|
|
253
|
+
this.loadData();
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
loadData() {
|
|
259
|
+
this.loading.set(true);
|
|
260
|
+
this.error.set(null);
|
|
261
|
+
const range = this.timeRange();
|
|
262
|
+
forkJoin({
|
|
263
|
+
metrics: this.telemetryService.getOverviewMetrics(range),
|
|
264
|
+
tokens: this.telemetryService.getMetricSeries('tokens_issued', range),
|
|
265
|
+
auth: this.telemetryService.getMetricSeries('auth_success', range),
|
|
266
|
+
}).subscribe({
|
|
267
|
+
next: ({ metrics, tokens, auth }) => {
|
|
268
|
+
this.metrics.set(metrics);
|
|
269
|
+
this.tokenSeries.set(tokens);
|
|
270
|
+
this.authSeries.set(auth);
|
|
271
|
+
this.loading.set(false);
|
|
272
|
+
},
|
|
273
|
+
error: (err) => {
|
|
274
|
+
this.error.set('Failed to load telemetry data');
|
|
275
|
+
console.error(err);
|
|
276
|
+
this.loading.set(false);
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
getBarHeight(value, data) {
|
|
281
|
+
const max = Math.max(...data.map((d) => d.value), 1);
|
|
282
|
+
return Math.max((value / max) * 100, 2);
|
|
283
|
+
}
|
|
284
|
+
formatChartLabel(timestamp) {
|
|
285
|
+
return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
286
|
+
}
|
|
287
|
+
getFirstLabel(data) {
|
|
288
|
+
return data.length > 0 ? this.formatChartLabel(data[0].timestamp) : '';
|
|
289
|
+
}
|
|
290
|
+
getLastLabel(data) {
|
|
291
|
+
return data.length > 0 ? this.formatChartLabel(data[data.length - 1].timestamp) : '';
|
|
292
|
+
}
|
|
293
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: TelemetryDashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
294
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: TelemetryDashboardComponent, isStandalone: true, selector: "lib-telemetry-dashboard", ngImport: i0, template: `
|
|
295
|
+
<div class="space-y-6">
|
|
296
|
+
<!-- Header -->
|
|
297
|
+
<div class="flex justify-between items-center">
|
|
298
|
+
<div>
|
|
299
|
+
<h1 class="text-2xl font-semibold text-gray-900">Telemetry Dashboard</h1>
|
|
300
|
+
<p class="text-gray-500 mt-1">
|
|
301
|
+
Monitor system health, performance, and security metrics
|
|
302
|
+
</p>
|
|
303
|
+
</div>
|
|
304
|
+
<div class="flex items-center gap-4">
|
|
305
|
+
<label class="flex items-center gap-2 text-sm text-gray-600">
|
|
306
|
+
<input
|
|
307
|
+
type="checkbox"
|
|
308
|
+
[checked]="autoRefresh()"
|
|
309
|
+
(change)="toggleAutoRefresh()"
|
|
310
|
+
class="rounded"
|
|
311
|
+
/>
|
|
312
|
+
Auto-refresh
|
|
313
|
+
</label>
|
|
314
|
+
<select
|
|
315
|
+
[ngModel]="timeRange()"
|
|
316
|
+
(ngModelChange)="setTimeRange($event)"
|
|
317
|
+
class="border rounded-md px-3 py-2 text-sm"
|
|
318
|
+
>
|
|
319
|
+
<option value="1h">Last 1 hour</option>
|
|
320
|
+
<option value="24h">Last 24 hours</option>
|
|
321
|
+
<option value="7d">Last 7 days</option>
|
|
322
|
+
<option value="30d">Last 30 days</option>
|
|
323
|
+
</select>
|
|
324
|
+
<button
|
|
325
|
+
(click)="loadData()"
|
|
326
|
+
[disabled]="loading()"
|
|
327
|
+
class="inline-flex items-center px-3 py-2 bg-gray-100 rounded-md hover:bg-gray-200 text-sm disabled:opacity-50"
|
|
328
|
+
>
|
|
329
|
+
<svg
|
|
330
|
+
class="h-4 w-4 mr-1"
|
|
331
|
+
[class.animate-spin]="loading()"
|
|
332
|
+
fill="none"
|
|
333
|
+
viewBox="0 0 24 24"
|
|
334
|
+
stroke="currentColor"
|
|
335
|
+
>
|
|
336
|
+
<path
|
|
337
|
+
stroke-linecap="round"
|
|
338
|
+
stroke-linejoin="round"
|
|
339
|
+
stroke-width="2"
|
|
340
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
341
|
+
/>
|
|
342
|
+
</svg>
|
|
343
|
+
Refresh
|
|
344
|
+
</button>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
@if (error()) {
|
|
349
|
+
<div class="bg-red-50 text-red-700 p-4 rounded-md">{{ error() }}</div>
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
<!-- Overview Metrics -->
|
|
353
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
354
|
+
<!-- Tokens Issued -->
|
|
355
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
356
|
+
<div class="flex items-center justify-between">
|
|
357
|
+
<div>
|
|
358
|
+
<p class="text-sm font-medium text-gray-500">Tokens Issued</p>
|
|
359
|
+
<p class="mt-1 text-2xl font-semibold text-gray-900">
|
|
360
|
+
{{ metrics()?.tokensIssued?.toLocaleString() ?? '-' }}
|
|
361
|
+
</p>
|
|
362
|
+
</div>
|
|
363
|
+
<div class="p-3 rounded-full bg-blue-50 text-blue-600">
|
|
364
|
+
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
365
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z" />
|
|
366
|
+
</svg>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
<!-- Active Users -->
|
|
372
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
373
|
+
<div class="flex items-center justify-between">
|
|
374
|
+
<div>
|
|
375
|
+
<p class="text-sm font-medium text-gray-500">Active Users</p>
|
|
376
|
+
<p class="mt-1 text-2xl font-semibold text-gray-900">
|
|
377
|
+
{{ metrics()?.activeUsers?.toLocaleString() ?? '-' }}
|
|
378
|
+
</p>
|
|
379
|
+
</div>
|
|
380
|
+
<div class="p-3 rounded-full bg-green-50 text-green-600">
|
|
381
|
+
<ui-icon name="user-group" class="h-6 w-6" />
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
<!-- Auth Success Rate -->
|
|
387
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
388
|
+
<div class="flex items-center justify-between">
|
|
389
|
+
<div>
|
|
390
|
+
<p class="text-sm font-medium text-gray-500">Auth Success Rate</p>
|
|
391
|
+
<p class="mt-1 text-2xl font-semibold text-gray-900">
|
|
392
|
+
{{ (metrics()?.authSuccessRate ?? 0).toFixed(1) }}%
|
|
393
|
+
</p>
|
|
394
|
+
</div>
|
|
395
|
+
<div
|
|
396
|
+
class="p-3 rounded-full"
|
|
397
|
+
[class.bg-green-50]="(metrics()?.authSuccessRate ?? 0) >= 95"
|
|
398
|
+
[class.text-green-600]="(metrics()?.authSuccessRate ?? 0) >= 95"
|
|
399
|
+
[class.bg-yellow-50]="(metrics()?.authSuccessRate ?? 0) < 95"
|
|
400
|
+
[class.text-yellow-600]="(metrics()?.authSuccessRate ?? 0) < 95"
|
|
401
|
+
>
|
|
402
|
+
<ui-icon name="check-circle" class="h-6 w-6" />
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
<!-- Avg Response Time -->
|
|
408
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
409
|
+
<div class="flex items-center justify-between">
|
|
410
|
+
<div>
|
|
411
|
+
<p class="text-sm font-medium text-gray-500">Avg Response Time</p>
|
|
412
|
+
<div class="mt-1 flex items-baseline">
|
|
413
|
+
<p class="text-2xl font-semibold text-gray-900">
|
|
414
|
+
{{ (metrics()?.avgRequestDuration ?? 0).toFixed(0) }}
|
|
415
|
+
</p>
|
|
416
|
+
<span class="ml-1 text-sm text-gray-500">ms</span>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
<div
|
|
420
|
+
class="p-3 rounded-full"
|
|
421
|
+
[class.bg-green-50]="(metrics()?.avgRequestDuration ?? 0) < 200"
|
|
422
|
+
[class.text-green-600]="(metrics()?.avgRequestDuration ?? 0) < 200"
|
|
423
|
+
[class.bg-yellow-50]="(metrics()?.avgRequestDuration ?? 0) >= 200"
|
|
424
|
+
[class.text-yellow-600]="(metrics()?.avgRequestDuration ?? 0) >= 200"
|
|
425
|
+
>
|
|
426
|
+
<ui-icon name="clock" class="h-6 w-6" />
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
<!-- Secondary Metrics -->
|
|
433
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
434
|
+
<!-- Token Failures -->
|
|
435
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
436
|
+
<div class="flex items-center justify-between">
|
|
437
|
+
<div>
|
|
438
|
+
<p class="text-sm font-medium text-gray-500">Token Failures</p>
|
|
439
|
+
<p class="mt-1 text-2xl font-semibold text-gray-900">
|
|
440
|
+
{{ metrics()?.tokensFailed?.toLocaleString() ?? '-' }}
|
|
441
|
+
</p>
|
|
442
|
+
</div>
|
|
443
|
+
<div class="p-3 rounded-full bg-red-50 text-red-600">
|
|
444
|
+
<ui-icon name="warning" class="h-6 w-6" />
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
<!-- Active Sessions -->
|
|
450
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
451
|
+
<div class="flex items-center justify-between">
|
|
452
|
+
<div>
|
|
453
|
+
<p class="text-sm font-medium text-gray-500">Active Sessions</p>
|
|
454
|
+
<p class="mt-1 text-2xl font-semibold text-gray-900">
|
|
455
|
+
{{ metrics()?.activeSessions?.toLocaleString() ?? '-' }}
|
|
456
|
+
</p>
|
|
457
|
+
</div>
|
|
458
|
+
<div class="p-3 rounded-full bg-purple-50 text-purple-600">
|
|
459
|
+
<ui-icon name="users" class="h-6 w-6" />
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
|
|
464
|
+
<!-- System Health -->
|
|
465
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
466
|
+
<div class="flex items-center justify-between">
|
|
467
|
+
<div>
|
|
468
|
+
<p class="text-sm font-medium text-gray-500">System Health</p>
|
|
469
|
+
<p class="mt-1 text-2xl font-semibold text-gray-900">
|
|
470
|
+
{{ systemHealth() }}
|
|
471
|
+
</p>
|
|
472
|
+
</div>
|
|
473
|
+
<div
|
|
474
|
+
class="p-3 rounded-full"
|
|
475
|
+
[class.bg-green-50]="systemHealth() === 'Healthy'"
|
|
476
|
+
[class.text-green-600]="systemHealth() === 'Healthy'"
|
|
477
|
+
[class.bg-yellow-50]="systemHealth() !== 'Healthy'"
|
|
478
|
+
[class.text-yellow-600]="systemHealth() !== 'Healthy'"
|
|
479
|
+
>
|
|
480
|
+
<ui-icon name="check-circle" class="h-6 w-6" />
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
|
|
486
|
+
<!-- Charts -->
|
|
487
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
488
|
+
<!-- Tokens Over Time -->
|
|
489
|
+
@if (tokenSeries()) {
|
|
490
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
491
|
+
<h3 class="text-lg font-medium text-gray-900 mb-4">Tokens Issued Over Time</h3>
|
|
492
|
+
<div class="h-48 flex items-end gap-1">
|
|
493
|
+
@for (point of tokenSeries()!.data; track point.timestamp) {
|
|
494
|
+
<div
|
|
495
|
+
class="flex-1 bg-blue-500 rounded-t hover:bg-blue-600 transition-colors"
|
|
496
|
+
[style.height.%]="getBarHeight(point.value, tokenSeries()!.data)"
|
|
497
|
+
[title]="formatChartLabel(point.timestamp) + ': ' + point.value"
|
|
498
|
+
></div>
|
|
499
|
+
}
|
|
500
|
+
</div>
|
|
501
|
+
<div class="flex justify-between mt-2 text-xs text-gray-500">
|
|
502
|
+
<span>{{ getFirstLabel(tokenSeries()!.data) }}</span>
|
|
503
|
+
<span>{{ getLastLabel(tokenSeries()!.data) }}</span>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
<!-- Auth Success Over Time -->
|
|
509
|
+
@if (authSeries()) {
|
|
510
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
511
|
+
<h3 class="text-lg font-medium text-gray-900 mb-4">Authentication Success Over Time</h3>
|
|
512
|
+
<div class="h-48 flex items-end gap-1">
|
|
513
|
+
@for (point of authSeries()!.data; track point.timestamp) {
|
|
514
|
+
<div
|
|
515
|
+
class="flex-1 bg-green-500 rounded-t hover:bg-green-600 transition-colors"
|
|
516
|
+
[style.height.%]="getBarHeight(point.value, authSeries()!.data)"
|
|
517
|
+
[title]="formatChartLabel(point.timestamp) + ': ' + point.value"
|
|
518
|
+
></div>
|
|
519
|
+
}
|
|
520
|
+
</div>
|
|
521
|
+
<div class="flex justify-between mt-2 text-xs text-gray-500">
|
|
522
|
+
<span>{{ getFirstLabel(authSeries()!.data) }}</span>
|
|
523
|
+
<span>{{ getLastLabel(authSeries()!.data) }}</span>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
}
|
|
527
|
+
</div>
|
|
528
|
+
|
|
529
|
+
<!-- Quick Links -->
|
|
530
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
531
|
+
<a
|
|
532
|
+
routerLink="/telemetry/logs"
|
|
533
|
+
class="block p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
|
|
534
|
+
>
|
|
535
|
+
<h3 class="font-medium text-gray-900">View Logs</h3>
|
|
536
|
+
<p class="text-sm text-gray-500 mt-1">Search and filter application logs</p>
|
|
537
|
+
</a>
|
|
538
|
+
<a
|
|
539
|
+
routerLink="/telemetry/traces"
|
|
540
|
+
class="block p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
|
|
541
|
+
>
|
|
542
|
+
<h3 class="font-medium text-gray-900">View Traces</h3>
|
|
543
|
+
<p class="text-sm text-gray-500 mt-1">Distributed tracing for requests</p>
|
|
544
|
+
</a>
|
|
545
|
+
<a
|
|
546
|
+
routerLink="/audit-logs"
|
|
547
|
+
class="block p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
|
|
548
|
+
>
|
|
549
|
+
<h3 class="font-medium text-gray-900">Audit Logs</h3>
|
|
550
|
+
<p class="text-sm text-gray-500 mt-1">Security and compliance audit trail</p>
|
|
551
|
+
</a>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: IconComponent, selector: "ui-icon", inputs: ["name", "strokeWidth"] }] });
|
|
555
|
+
}
|
|
556
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: TelemetryDashboardComponent, decorators: [{
|
|
557
|
+
type: Component,
|
|
558
|
+
args: [{
|
|
559
|
+
selector: 'lib-telemetry-dashboard',
|
|
560
|
+
standalone: true,
|
|
561
|
+
imports: [CommonModule, FormsModule, RouterLink, IconComponent],
|
|
562
|
+
template: `
|
|
563
|
+
<div class="space-y-6">
|
|
564
|
+
<!-- Header -->
|
|
565
|
+
<div class="flex justify-between items-center">
|
|
566
|
+
<div>
|
|
567
|
+
<h1 class="text-2xl font-semibold text-gray-900">Telemetry Dashboard</h1>
|
|
568
|
+
<p class="text-gray-500 mt-1">
|
|
569
|
+
Monitor system health, performance, and security metrics
|
|
570
|
+
</p>
|
|
571
|
+
</div>
|
|
572
|
+
<div class="flex items-center gap-4">
|
|
573
|
+
<label class="flex items-center gap-2 text-sm text-gray-600">
|
|
574
|
+
<input
|
|
575
|
+
type="checkbox"
|
|
576
|
+
[checked]="autoRefresh()"
|
|
577
|
+
(change)="toggleAutoRefresh()"
|
|
578
|
+
class="rounded"
|
|
579
|
+
/>
|
|
580
|
+
Auto-refresh
|
|
581
|
+
</label>
|
|
582
|
+
<select
|
|
583
|
+
[ngModel]="timeRange()"
|
|
584
|
+
(ngModelChange)="setTimeRange($event)"
|
|
585
|
+
class="border rounded-md px-3 py-2 text-sm"
|
|
586
|
+
>
|
|
587
|
+
<option value="1h">Last 1 hour</option>
|
|
588
|
+
<option value="24h">Last 24 hours</option>
|
|
589
|
+
<option value="7d">Last 7 days</option>
|
|
590
|
+
<option value="30d">Last 30 days</option>
|
|
591
|
+
</select>
|
|
592
|
+
<button
|
|
593
|
+
(click)="loadData()"
|
|
594
|
+
[disabled]="loading()"
|
|
595
|
+
class="inline-flex items-center px-3 py-2 bg-gray-100 rounded-md hover:bg-gray-200 text-sm disabled:opacity-50"
|
|
596
|
+
>
|
|
597
|
+
<svg
|
|
598
|
+
class="h-4 w-4 mr-1"
|
|
599
|
+
[class.animate-spin]="loading()"
|
|
600
|
+
fill="none"
|
|
601
|
+
viewBox="0 0 24 24"
|
|
602
|
+
stroke="currentColor"
|
|
603
|
+
>
|
|
604
|
+
<path
|
|
605
|
+
stroke-linecap="round"
|
|
606
|
+
stroke-linejoin="round"
|
|
607
|
+
stroke-width="2"
|
|
608
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
609
|
+
/>
|
|
610
|
+
</svg>
|
|
611
|
+
Refresh
|
|
612
|
+
</button>
|
|
613
|
+
</div>
|
|
614
|
+
</div>
|
|
615
|
+
|
|
616
|
+
@if (error()) {
|
|
617
|
+
<div class="bg-red-50 text-red-700 p-4 rounded-md">{{ error() }}</div>
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
<!-- Overview Metrics -->
|
|
621
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
622
|
+
<!-- Tokens Issued -->
|
|
623
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
624
|
+
<div class="flex items-center justify-between">
|
|
625
|
+
<div>
|
|
626
|
+
<p class="text-sm font-medium text-gray-500">Tokens Issued</p>
|
|
627
|
+
<p class="mt-1 text-2xl font-semibold text-gray-900">
|
|
628
|
+
{{ metrics()?.tokensIssued?.toLocaleString() ?? '-' }}
|
|
629
|
+
</p>
|
|
630
|
+
</div>
|
|
631
|
+
<div class="p-3 rounded-full bg-blue-50 text-blue-600">
|
|
632
|
+
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
633
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z" />
|
|
634
|
+
</svg>
|
|
635
|
+
</div>
|
|
636
|
+
</div>
|
|
637
|
+
</div>
|
|
638
|
+
|
|
639
|
+
<!-- Active Users -->
|
|
640
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
641
|
+
<div class="flex items-center justify-between">
|
|
642
|
+
<div>
|
|
643
|
+
<p class="text-sm font-medium text-gray-500">Active Users</p>
|
|
644
|
+
<p class="mt-1 text-2xl font-semibold text-gray-900">
|
|
645
|
+
{{ metrics()?.activeUsers?.toLocaleString() ?? '-' }}
|
|
646
|
+
</p>
|
|
647
|
+
</div>
|
|
648
|
+
<div class="p-3 rounded-full bg-green-50 text-green-600">
|
|
649
|
+
<ui-icon name="user-group" class="h-6 w-6" />
|
|
650
|
+
</div>
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
|
|
654
|
+
<!-- Auth Success Rate -->
|
|
655
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
656
|
+
<div class="flex items-center justify-between">
|
|
657
|
+
<div>
|
|
658
|
+
<p class="text-sm font-medium text-gray-500">Auth Success Rate</p>
|
|
659
|
+
<p class="mt-1 text-2xl font-semibold text-gray-900">
|
|
660
|
+
{{ (metrics()?.authSuccessRate ?? 0).toFixed(1) }}%
|
|
661
|
+
</p>
|
|
662
|
+
</div>
|
|
663
|
+
<div
|
|
664
|
+
class="p-3 rounded-full"
|
|
665
|
+
[class.bg-green-50]="(metrics()?.authSuccessRate ?? 0) >= 95"
|
|
666
|
+
[class.text-green-600]="(metrics()?.authSuccessRate ?? 0) >= 95"
|
|
667
|
+
[class.bg-yellow-50]="(metrics()?.authSuccessRate ?? 0) < 95"
|
|
668
|
+
[class.text-yellow-600]="(metrics()?.authSuccessRate ?? 0) < 95"
|
|
669
|
+
>
|
|
670
|
+
<ui-icon name="check-circle" class="h-6 w-6" />
|
|
671
|
+
</div>
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
|
|
675
|
+
<!-- Avg Response Time -->
|
|
676
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
677
|
+
<div class="flex items-center justify-between">
|
|
678
|
+
<div>
|
|
679
|
+
<p class="text-sm font-medium text-gray-500">Avg Response Time</p>
|
|
680
|
+
<div class="mt-1 flex items-baseline">
|
|
681
|
+
<p class="text-2xl font-semibold text-gray-900">
|
|
682
|
+
{{ (metrics()?.avgRequestDuration ?? 0).toFixed(0) }}
|
|
683
|
+
</p>
|
|
684
|
+
<span class="ml-1 text-sm text-gray-500">ms</span>
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
<div
|
|
688
|
+
class="p-3 rounded-full"
|
|
689
|
+
[class.bg-green-50]="(metrics()?.avgRequestDuration ?? 0) < 200"
|
|
690
|
+
[class.text-green-600]="(metrics()?.avgRequestDuration ?? 0) < 200"
|
|
691
|
+
[class.bg-yellow-50]="(metrics()?.avgRequestDuration ?? 0) >= 200"
|
|
692
|
+
[class.text-yellow-600]="(metrics()?.avgRequestDuration ?? 0) >= 200"
|
|
693
|
+
>
|
|
694
|
+
<ui-icon name="clock" class="h-6 w-6" />
|
|
695
|
+
</div>
|
|
696
|
+
</div>
|
|
697
|
+
</div>
|
|
698
|
+
</div>
|
|
699
|
+
|
|
700
|
+
<!-- Secondary Metrics -->
|
|
701
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
702
|
+
<!-- Token Failures -->
|
|
703
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
704
|
+
<div class="flex items-center justify-between">
|
|
705
|
+
<div>
|
|
706
|
+
<p class="text-sm font-medium text-gray-500">Token Failures</p>
|
|
707
|
+
<p class="mt-1 text-2xl font-semibold text-gray-900">
|
|
708
|
+
{{ metrics()?.tokensFailed?.toLocaleString() ?? '-' }}
|
|
709
|
+
</p>
|
|
710
|
+
</div>
|
|
711
|
+
<div class="p-3 rounded-full bg-red-50 text-red-600">
|
|
712
|
+
<ui-icon name="warning" class="h-6 w-6" />
|
|
713
|
+
</div>
|
|
714
|
+
</div>
|
|
715
|
+
</div>
|
|
716
|
+
|
|
717
|
+
<!-- Active Sessions -->
|
|
718
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
719
|
+
<div class="flex items-center justify-between">
|
|
720
|
+
<div>
|
|
721
|
+
<p class="text-sm font-medium text-gray-500">Active Sessions</p>
|
|
722
|
+
<p class="mt-1 text-2xl font-semibold text-gray-900">
|
|
723
|
+
{{ metrics()?.activeSessions?.toLocaleString() ?? '-' }}
|
|
724
|
+
</p>
|
|
725
|
+
</div>
|
|
726
|
+
<div class="p-3 rounded-full bg-purple-50 text-purple-600">
|
|
727
|
+
<ui-icon name="users" class="h-6 w-6" />
|
|
728
|
+
</div>
|
|
729
|
+
</div>
|
|
730
|
+
</div>
|
|
731
|
+
|
|
732
|
+
<!-- System Health -->
|
|
733
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
734
|
+
<div class="flex items-center justify-between">
|
|
735
|
+
<div>
|
|
736
|
+
<p class="text-sm font-medium text-gray-500">System Health</p>
|
|
737
|
+
<p class="mt-1 text-2xl font-semibold text-gray-900">
|
|
738
|
+
{{ systemHealth() }}
|
|
739
|
+
</p>
|
|
740
|
+
</div>
|
|
741
|
+
<div
|
|
742
|
+
class="p-3 rounded-full"
|
|
743
|
+
[class.bg-green-50]="systemHealth() === 'Healthy'"
|
|
744
|
+
[class.text-green-600]="systemHealth() === 'Healthy'"
|
|
745
|
+
[class.bg-yellow-50]="systemHealth() !== 'Healthy'"
|
|
746
|
+
[class.text-yellow-600]="systemHealth() !== 'Healthy'"
|
|
747
|
+
>
|
|
748
|
+
<ui-icon name="check-circle" class="h-6 w-6" />
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
</div>
|
|
752
|
+
</div>
|
|
753
|
+
|
|
754
|
+
<!-- Charts -->
|
|
755
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
756
|
+
<!-- Tokens Over Time -->
|
|
757
|
+
@if (tokenSeries()) {
|
|
758
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
759
|
+
<h3 class="text-lg font-medium text-gray-900 mb-4">Tokens Issued Over Time</h3>
|
|
760
|
+
<div class="h-48 flex items-end gap-1">
|
|
761
|
+
@for (point of tokenSeries()!.data; track point.timestamp) {
|
|
762
|
+
<div
|
|
763
|
+
class="flex-1 bg-blue-500 rounded-t hover:bg-blue-600 transition-colors"
|
|
764
|
+
[style.height.%]="getBarHeight(point.value, tokenSeries()!.data)"
|
|
765
|
+
[title]="formatChartLabel(point.timestamp) + ': ' + point.value"
|
|
766
|
+
></div>
|
|
767
|
+
}
|
|
768
|
+
</div>
|
|
769
|
+
<div class="flex justify-between mt-2 text-xs text-gray-500">
|
|
770
|
+
<span>{{ getFirstLabel(tokenSeries()!.data) }}</span>
|
|
771
|
+
<span>{{ getLastLabel(tokenSeries()!.data) }}</span>
|
|
772
|
+
</div>
|
|
773
|
+
</div>
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
<!-- Auth Success Over Time -->
|
|
777
|
+
@if (authSeries()) {
|
|
778
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
779
|
+
<h3 class="text-lg font-medium text-gray-900 mb-4">Authentication Success Over Time</h3>
|
|
780
|
+
<div class="h-48 flex items-end gap-1">
|
|
781
|
+
@for (point of authSeries()!.data; track point.timestamp) {
|
|
782
|
+
<div
|
|
783
|
+
class="flex-1 bg-green-500 rounded-t hover:bg-green-600 transition-colors"
|
|
784
|
+
[style.height.%]="getBarHeight(point.value, authSeries()!.data)"
|
|
785
|
+
[title]="formatChartLabel(point.timestamp) + ': ' + point.value"
|
|
786
|
+
></div>
|
|
787
|
+
}
|
|
788
|
+
</div>
|
|
789
|
+
<div class="flex justify-between mt-2 text-xs text-gray-500">
|
|
790
|
+
<span>{{ getFirstLabel(authSeries()!.data) }}</span>
|
|
791
|
+
<span>{{ getLastLabel(authSeries()!.data) }}</span>
|
|
792
|
+
</div>
|
|
793
|
+
</div>
|
|
794
|
+
}
|
|
795
|
+
</div>
|
|
796
|
+
|
|
797
|
+
<!-- Quick Links -->
|
|
798
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
799
|
+
<a
|
|
800
|
+
routerLink="/telemetry/logs"
|
|
801
|
+
class="block p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
|
|
802
|
+
>
|
|
803
|
+
<h3 class="font-medium text-gray-900">View Logs</h3>
|
|
804
|
+
<p class="text-sm text-gray-500 mt-1">Search and filter application logs</p>
|
|
805
|
+
</a>
|
|
806
|
+
<a
|
|
807
|
+
routerLink="/telemetry/traces"
|
|
808
|
+
class="block p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
|
|
809
|
+
>
|
|
810
|
+
<h3 class="font-medium text-gray-900">View Traces</h3>
|
|
811
|
+
<p class="text-sm text-gray-500 mt-1">Distributed tracing for requests</p>
|
|
812
|
+
</a>
|
|
813
|
+
<a
|
|
814
|
+
routerLink="/audit-logs"
|
|
815
|
+
class="block p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
|
|
816
|
+
>
|
|
817
|
+
<h3 class="font-medium text-gray-900">Audit Logs</h3>
|
|
818
|
+
<p class="text-sm text-gray-500 mt-1">Security and compliance audit trail</p>
|
|
819
|
+
</a>
|
|
820
|
+
</div>
|
|
821
|
+
</div>
|
|
822
|
+
`,
|
|
823
|
+
}]
|
|
824
|
+
}] });
|
|
825
|
+
|
|
826
|
+
var telemetryDashboard_component = /*#__PURE__*/Object.freeze({
|
|
827
|
+
__proto__: null,
|
|
828
|
+
TelemetryDashboardComponent: TelemetryDashboardComponent
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
class TelemetryLogsComponent {
|
|
832
|
+
telemetryService = inject(TelemetryService);
|
|
833
|
+
router = inject(Router);
|
|
834
|
+
route = inject(ActivatedRoute);
|
|
835
|
+
destroy$ = new Subject();
|
|
836
|
+
logs = signal([], ...(ngDevMode ? [{ debugName: "logs" }] : /* istanbul ignore next */ []));
|
|
837
|
+
categories = signal([], ...(ngDevMode ? [{ debugName: "categories" }] : /* istanbul ignore next */ []));
|
|
838
|
+
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
839
|
+
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
840
|
+
totalCount = signal(0, ...(ngDevMode ? [{ debugName: "totalCount" }] : /* istanbul ignore next */ []));
|
|
841
|
+
liveTail = signal(false, ...(ngDevMode ? [{ debugName: "liveTail" }] : /* istanbul ignore next */ []));
|
|
842
|
+
expandedLogs = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedLogs" }] : /* istanbul ignore next */ []));
|
|
843
|
+
// Theme preference stored in localStorage
|
|
844
|
+
THEME_KEY = 'telemetry-logs-theme';
|
|
845
|
+
darkMode = signal(this.loadThemePreference(), ...(ngDevMode ? [{ debugName: "darkMode" }] : /* istanbul ignore next */ []));
|
|
846
|
+
// Filters
|
|
847
|
+
searchTerm = '';
|
|
848
|
+
level = '';
|
|
849
|
+
category = '';
|
|
850
|
+
currentPage = signal(1, ...(ngDevMode ? [{ debugName: "currentPage" }] : /* istanbul ignore next */ []));
|
|
851
|
+
pageSize = 50;
|
|
852
|
+
totalPages = computed(() => Math.ceil(this.totalCount() / this.pageSize) || 1, ...(ngDevMode ? [{ debugName: "totalPages" }] : /* istanbul ignore next */ []));
|
|
853
|
+
// Expose Math for template
|
|
854
|
+
Math = Math;
|
|
855
|
+
// Backend label rendered in the header. Null until the status endpoint
|
|
856
|
+
// resolves so we don't flash an incorrect badge during the round-trip.
|
|
857
|
+
backend = signal(null, ...(ngDevMode ? [{ debugName: "backend" }] : /* istanbul ignore next */ []));
|
|
858
|
+
ngOnInit() {
|
|
859
|
+
// Read initial filters from query params
|
|
860
|
+
this.searchTerm = this.route.snapshot.queryParams['search'] || '';
|
|
861
|
+
this.level = this.route.snapshot.queryParams['level'] || '';
|
|
862
|
+
this.category = this.route.snapshot.queryParams['category'] || '';
|
|
863
|
+
this.loadBackendStatus();
|
|
864
|
+
this.loadCategories();
|
|
865
|
+
this.loadLogs();
|
|
866
|
+
}
|
|
867
|
+
loadBackendStatus() {
|
|
868
|
+
this.telemetryService.getLogBackendStatus().subscribe({
|
|
869
|
+
next: (status) => this.backend.set(status.isConfigured ? status.backend : null),
|
|
870
|
+
error: () => {
|
|
871
|
+
// Status endpoint missing in older deployments — leave badge hidden
|
|
872
|
+
// rather than guessing. The rest of the page still works.
|
|
873
|
+
this.backend.set(null);
|
|
874
|
+
},
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
ngOnDestroy() {
|
|
878
|
+
this.destroy$.next();
|
|
879
|
+
this.destroy$.complete();
|
|
880
|
+
}
|
|
881
|
+
loadCategories() {
|
|
882
|
+
this.telemetryService.getLogCategories().subscribe({
|
|
883
|
+
next: (cats) => this.categories.set(cats),
|
|
884
|
+
error: (err) => console.error('Failed to load categories:', err),
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
loadLogs(silent = false) {
|
|
888
|
+
if (!silent) {
|
|
889
|
+
this.loading.set(true);
|
|
890
|
+
}
|
|
891
|
+
this.error.set(null);
|
|
892
|
+
this.telemetryService
|
|
893
|
+
.getLogs({
|
|
894
|
+
searchTerm: this.searchTerm || undefined,
|
|
895
|
+
level: this.level || undefined,
|
|
896
|
+
category: this.category || undefined,
|
|
897
|
+
page: this.currentPage(),
|
|
898
|
+
pageSize: this.pageSize,
|
|
899
|
+
})
|
|
900
|
+
.subscribe({
|
|
901
|
+
next: (result) => {
|
|
902
|
+
this.logs.set(result.items);
|
|
903
|
+
this.totalCount.set(result.totalCount);
|
|
904
|
+
this.loading.set(false);
|
|
905
|
+
},
|
|
906
|
+
error: (err) => {
|
|
907
|
+
if (!silent) {
|
|
908
|
+
this.error.set('Failed to load logs');
|
|
909
|
+
}
|
|
910
|
+
console.error(err);
|
|
911
|
+
this.loading.set(false);
|
|
912
|
+
},
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
handleSearch() {
|
|
916
|
+
this.currentPage.set(1);
|
|
917
|
+
// Update URL params
|
|
918
|
+
this.router.navigate([], {
|
|
919
|
+
relativeTo: this.route,
|
|
920
|
+
queryParams: {
|
|
921
|
+
search: this.searchTerm || null,
|
|
922
|
+
level: this.level || null,
|
|
923
|
+
category: this.category || null,
|
|
924
|
+
},
|
|
925
|
+
queryParamsHandling: 'merge',
|
|
926
|
+
});
|
|
927
|
+
this.loadLogs();
|
|
928
|
+
}
|
|
929
|
+
toggleLiveTail() {
|
|
930
|
+
this.liveTail.update((v) => !v);
|
|
931
|
+
if (this.liveTail()) {
|
|
932
|
+
interval(2000)
|
|
933
|
+
.pipe(takeUntil(this.destroy$))
|
|
934
|
+
.subscribe(() => {
|
|
935
|
+
if (this.liveTail()) {
|
|
936
|
+
this.loadLogs(true); // Silent refresh to avoid flicker
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
toggleExpand(logId) {
|
|
942
|
+
this.expandedLogs.update((set) => {
|
|
943
|
+
const newSet = new Set(set);
|
|
944
|
+
if (newSet.has(logId)) {
|
|
945
|
+
newSet.delete(logId);
|
|
946
|
+
}
|
|
947
|
+
else {
|
|
948
|
+
newSet.add(logId);
|
|
949
|
+
}
|
|
950
|
+
return newSet;
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
hasDetails(log) {
|
|
954
|
+
return !!(log.properties ||
|
|
955
|
+
log.exception ||
|
|
956
|
+
log.exceptionType ||
|
|
957
|
+
log.stackTrace ||
|
|
958
|
+
log.traceId ||
|
|
959
|
+
log.requestPath ||
|
|
960
|
+
log.userId ||
|
|
961
|
+
log.clientId ||
|
|
962
|
+
log.machineName);
|
|
963
|
+
}
|
|
964
|
+
hasProperties(props) {
|
|
965
|
+
return Object.keys(props).length > 0;
|
|
966
|
+
}
|
|
967
|
+
goToPage(page) {
|
|
968
|
+
if (page >= 1 && page <= this.totalPages()) {
|
|
969
|
+
this.currentPage.set(page);
|
|
970
|
+
this.loadLogs();
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
navigateToTrace(traceId) {
|
|
974
|
+
this.router.navigate(['/telemetry/traces', traceId]);
|
|
975
|
+
}
|
|
976
|
+
formatDate(dateString) {
|
|
977
|
+
return new Date(dateString).toLocaleString();
|
|
978
|
+
}
|
|
979
|
+
formatJson(obj) {
|
|
980
|
+
return JSON.stringify(obj, null, 2);
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Normalize log level from backend format to frontend format
|
|
984
|
+
* Backend returns: "Trace", "Debug", "Information", "Warning", "Error", "Critical"
|
|
985
|
+
* Frontend expects: "trace", "debug", "info", "warn", "error", "fatal"
|
|
986
|
+
*/
|
|
987
|
+
normalizeLevel(level) {
|
|
988
|
+
const levelMap = {
|
|
989
|
+
'trace': 'trace',
|
|
990
|
+
'debug': 'debug',
|
|
991
|
+
'information': 'info',
|
|
992
|
+
'info': 'info',
|
|
993
|
+
'warning': 'warn',
|
|
994
|
+
'warn': 'warn',
|
|
995
|
+
'error': 'error',
|
|
996
|
+
'critical': 'fatal',
|
|
997
|
+
'fatal': 'fatal',
|
|
998
|
+
};
|
|
999
|
+
return levelMap[level.toLowerCase()] || 'info';
|
|
1000
|
+
}
|
|
1001
|
+
getLevelClasses(level) {
|
|
1002
|
+
const normalizedLevel = this.normalizeLevel(level);
|
|
1003
|
+
const config = LOG_LEVEL_CONFIG[normalizedLevel];
|
|
1004
|
+
return `${config.bgColor} ${config.textColor}`;
|
|
1005
|
+
}
|
|
1006
|
+
getLevelLabel(level) {
|
|
1007
|
+
const normalizedLevel = this.normalizeLevel(level);
|
|
1008
|
+
return LOG_LEVEL_CONFIG[normalizedLevel].label;
|
|
1009
|
+
}
|
|
1010
|
+
getLevelStyles(level) {
|
|
1011
|
+
const normalizedLevel = this.normalizeLevel(level);
|
|
1012
|
+
const styles = {
|
|
1013
|
+
trace: { bg: '#374151', color: '#d1d5db' },
|
|
1014
|
+
debug: { bg: '#4b5563', color: '#d1d5db' },
|
|
1015
|
+
info: { bg: '#1e3a5f', color: '#93c5fd' },
|
|
1016
|
+
warn: { bg: '#713f12', color: '#fde047' },
|
|
1017
|
+
error: { bg: '#7f1d1d', color: '#fca5a5' },
|
|
1018
|
+
fatal: { bg: '#991b1b', color: '#fef2f2' },
|
|
1019
|
+
};
|
|
1020
|
+
const style = styles[normalizedLevel];
|
|
1021
|
+
return { backgroundColor: style.bg, color: style.color };
|
|
1022
|
+
}
|
|
1023
|
+
// Theme management
|
|
1024
|
+
loadThemePreference() {
|
|
1025
|
+
if (typeof localStorage !== 'undefined') {
|
|
1026
|
+
const saved = localStorage.getItem(this.THEME_KEY);
|
|
1027
|
+
return saved === null ? true : saved === 'dark'; // Default to dark
|
|
1028
|
+
}
|
|
1029
|
+
return true;
|
|
1030
|
+
}
|
|
1031
|
+
toggleTheme() {
|
|
1032
|
+
this.darkMode.update((v) => !v);
|
|
1033
|
+
if (typeof localStorage !== 'undefined') {
|
|
1034
|
+
localStorage.setItem(this.THEME_KEY, this.darkMode() ? 'dark' : 'light');
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
// Get dynamic classes for dark/light mode - true black terminal style for dark mode
|
|
1038
|
+
getLogViewerClasses() {
|
|
1039
|
+
return this.darkMode()
|
|
1040
|
+
? 'bg-black text-gray-100'
|
|
1041
|
+
: 'bg-white text-gray-900 border border-gray-200';
|
|
1042
|
+
}
|
|
1043
|
+
getLogRowClasses() {
|
|
1044
|
+
return this.darkMode()
|
|
1045
|
+
? 'border-b border-gray-800 hover:bg-gray-900'
|
|
1046
|
+
: 'border-b border-gray-200 hover:bg-gray-50';
|
|
1047
|
+
}
|
|
1048
|
+
getTimestampClasses() {
|
|
1049
|
+
return this.darkMode() ? 'text-gray-400' : 'text-gray-500';
|
|
1050
|
+
}
|
|
1051
|
+
getCategoryClasses() {
|
|
1052
|
+
return this.darkMode() ? 'text-fuchsia-400' : 'text-purple-600';
|
|
1053
|
+
}
|
|
1054
|
+
getMessageClasses() {
|
|
1055
|
+
return this.darkMode() ? 'text-white' : 'text-gray-800';
|
|
1056
|
+
}
|
|
1057
|
+
getExpandIconClasses() {
|
|
1058
|
+
return this.darkMode() ? 'text-gray-500' : 'text-gray-500';
|
|
1059
|
+
}
|
|
1060
|
+
getDetailsContainerClasses() {
|
|
1061
|
+
return this.darkMode() ? 'text-gray-200' : 'text-gray-700';
|
|
1062
|
+
}
|
|
1063
|
+
getPropertiesPreClasses() {
|
|
1064
|
+
return this.darkMode()
|
|
1065
|
+
? 'text-emerald-400 bg-black border-gray-800'
|
|
1066
|
+
: 'text-green-700 bg-gray-50 border-gray-200';
|
|
1067
|
+
}
|
|
1068
|
+
getExceptionPreClasses() {
|
|
1069
|
+
return this.darkMode()
|
|
1070
|
+
? 'text-red-400 bg-red-950/50 border-red-900'
|
|
1071
|
+
: 'text-red-700 bg-red-50 border-red-200';
|
|
1072
|
+
}
|
|
1073
|
+
getStackTracePreClasses() {
|
|
1074
|
+
return this.darkMode()
|
|
1075
|
+
? 'text-orange-300 bg-black border-gray-800'
|
|
1076
|
+
: 'text-red-700 bg-gray-50 border-gray-200';
|
|
1077
|
+
}
|
|
1078
|
+
getLabelClasses() {
|
|
1079
|
+
return this.darkMode() ? 'text-gray-500' : 'text-gray-400';
|
|
1080
|
+
}
|
|
1081
|
+
getContextValueClasses(type) {
|
|
1082
|
+
if (this.darkMode()) {
|
|
1083
|
+
switch (type) {
|
|
1084
|
+
case 'request': return 'text-cyan-300';
|
|
1085
|
+
case 'user': return 'text-emerald-400';
|
|
1086
|
+
case 'client': return 'text-amber-400';
|
|
1087
|
+
case 'machine': return 'text-gray-300';
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
else {
|
|
1091
|
+
switch (type) {
|
|
1092
|
+
case 'request': return 'text-cyan-600';
|
|
1093
|
+
case 'user': return 'text-green-600';
|
|
1094
|
+
case 'client': return 'text-yellow-600';
|
|
1095
|
+
case 'machine': return 'text-gray-600';
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
getTraceLinkClasses() {
|
|
1100
|
+
return this.darkMode()
|
|
1101
|
+
? 'text-sky-400 hover:text-sky-300 hover:underline font-mono'
|
|
1102
|
+
: 'text-blue-600 hover:underline font-mono';
|
|
1103
|
+
}
|
|
1104
|
+
getSpanIdClasses() {
|
|
1105
|
+
return this.darkMode() ? 'text-gray-400 font-mono' : 'text-gray-500 font-mono';
|
|
1106
|
+
}
|
|
1107
|
+
getExceptionLabelClasses() {
|
|
1108
|
+
return this.darkMode() ? 'text-red-500 font-semibold' : 'text-red-600 font-semibold';
|
|
1109
|
+
}
|
|
1110
|
+
getExceptionTypeClasses() {
|
|
1111
|
+
return this.darkMode() ? 'text-red-400 font-mono' : 'text-red-600 font-mono';
|
|
1112
|
+
}
|
|
1113
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: TelemetryLogsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1114
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: TelemetryLogsComponent, isStandalone: true, selector: "lib-telemetry-logs", ngImport: i0, template: `
|
|
1115
|
+
<div class="space-y-6">
|
|
1116
|
+
<!-- Header -->
|
|
1117
|
+
<div class="flex justify-between items-center">
|
|
1118
|
+
<div>
|
|
1119
|
+
<div class="flex items-center gap-2">
|
|
1120
|
+
<h1 class="text-2xl font-semibold text-gray-900">Application Logs</h1>
|
|
1121
|
+
@if (backend(); as b) {
|
|
1122
|
+
<span
|
|
1123
|
+
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"
|
|
1124
|
+
[class.bg-violet-100]="b === 'loki'"
|
|
1125
|
+
[class.text-violet-700]="b === 'loki'"
|
|
1126
|
+
[class.bg-gray-100]="b !== 'loki'"
|
|
1127
|
+
[class.text-gray-600]="b !== 'loki'"
|
|
1128
|
+
[title]="'Reading from ' + b"
|
|
1129
|
+
>
|
|
1130
|
+
{{ b }}
|
|
1131
|
+
</span>
|
|
1132
|
+
}
|
|
1133
|
+
</div>
|
|
1134
|
+
<p class="text-gray-500 mt-1">Search and filter logs from all services</p>
|
|
1135
|
+
</div>
|
|
1136
|
+
<div class="flex items-center gap-2">
|
|
1137
|
+
<!-- Theme toggle -->
|
|
1138
|
+
<button
|
|
1139
|
+
(click)="toggleTheme()"
|
|
1140
|
+
class="inline-flex items-center px-3 py-2 rounded-md text-sm bg-gray-100 text-gray-700 hover:bg-gray-200"
|
|
1141
|
+
[title]="darkMode() ? 'Switch to light mode' : 'Switch to dark mode'"
|
|
1142
|
+
>
|
|
1143
|
+
@if (darkMode()) {
|
|
1144
|
+
<ui-icon name="sun" class="h-4 w-4" />
|
|
1145
|
+
} @else {
|
|
1146
|
+
<ui-icon name="moon" class="h-4 w-4" />
|
|
1147
|
+
}
|
|
1148
|
+
</button>
|
|
1149
|
+
<button
|
|
1150
|
+
(click)="toggleLiveTail()"
|
|
1151
|
+
class="inline-flex items-center px-3 py-2 rounded-md text-sm"
|
|
1152
|
+
[class.bg-green-600]="liveTail()"
|
|
1153
|
+
[class.text-white]="liveTail()"
|
|
1154
|
+
[class.bg-gray-100]="!liveTail()"
|
|
1155
|
+
[class.text-gray-700]="!liveTail()"
|
|
1156
|
+
[class.hover:bg-gray-200]="!liveTail()"
|
|
1157
|
+
>
|
|
1158
|
+
@if (liveTail()) {
|
|
1159
|
+
<ui-icon name="stop-circle" class="h-4 w-4 mr-1" />
|
|
1160
|
+
Stop Live Tail
|
|
1161
|
+
} @else {
|
|
1162
|
+
<ui-icon name="play-circle" class="h-4 w-4 mr-1" />
|
|
1163
|
+
Live Tail
|
|
1164
|
+
}
|
|
1165
|
+
</button>
|
|
1166
|
+
<button
|
|
1167
|
+
(click)="loadLogs()"
|
|
1168
|
+
[disabled]="loading()"
|
|
1169
|
+
class="inline-flex items-center px-3 py-2 bg-gray-100 rounded-md hover:bg-gray-200 text-sm disabled:opacity-50"
|
|
1170
|
+
>
|
|
1171
|
+
<svg
|
|
1172
|
+
class="h-4 w-4 mr-1"
|
|
1173
|
+
[class.animate-spin]="loading()"
|
|
1174
|
+
fill="none"
|
|
1175
|
+
viewBox="0 0 24 24"
|
|
1176
|
+
stroke="currentColor"
|
|
1177
|
+
>
|
|
1178
|
+
<path
|
|
1179
|
+
stroke-linecap="round"
|
|
1180
|
+
stroke-linejoin="round"
|
|
1181
|
+
stroke-width="2"
|
|
1182
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
1183
|
+
/>
|
|
1184
|
+
</svg>
|
|
1185
|
+
Refresh
|
|
1186
|
+
</button>
|
|
1187
|
+
</div>
|
|
1188
|
+
</div>
|
|
1189
|
+
|
|
1190
|
+
<!-- Filters -->
|
|
1191
|
+
<form (ngSubmit)="handleSearch()" class="flex gap-4 items-end">
|
|
1192
|
+
<div class="flex-1">
|
|
1193
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Search</label>
|
|
1194
|
+
<div class="relative">
|
|
1195
|
+
<ui-icon name="search" class="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
|
1196
|
+
<input
|
|
1197
|
+
type="text"
|
|
1198
|
+
[(ngModel)]="searchTerm"
|
|
1199
|
+
name="searchTerm"
|
|
1200
|
+
placeholder="Search logs..."
|
|
1201
|
+
class="w-full pl-10 pr-4 py-2 border rounded-md"
|
|
1202
|
+
/>
|
|
1203
|
+
</div>
|
|
1204
|
+
</div>
|
|
1205
|
+
<div class="w-40">
|
|
1206
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Level</label>
|
|
1207
|
+
<select
|
|
1208
|
+
[(ngModel)]="level"
|
|
1209
|
+
name="level"
|
|
1210
|
+
class="w-full border rounded-md px-3 py-2"
|
|
1211
|
+
>
|
|
1212
|
+
<option value="">All Levels</option>
|
|
1213
|
+
<option value="trace">Trace</option>
|
|
1214
|
+
<option value="debug">Debug</option>
|
|
1215
|
+
<option value="info">Info</option>
|
|
1216
|
+
<option value="warn">Warning</option>
|
|
1217
|
+
<option value="error">Error</option>
|
|
1218
|
+
<option value="fatal">Fatal</option>
|
|
1219
|
+
</select>
|
|
1220
|
+
</div>
|
|
1221
|
+
<div class="w-48">
|
|
1222
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Category</label>
|
|
1223
|
+
<select
|
|
1224
|
+
[(ngModel)]="category"
|
|
1225
|
+
name="category"
|
|
1226
|
+
class="w-full border rounded-md px-3 py-2"
|
|
1227
|
+
>
|
|
1228
|
+
<option value="">All Categories</option>
|
|
1229
|
+
@for (cat of categories(); track cat) {
|
|
1230
|
+
<option [value]="cat">{{ cat }}</option>
|
|
1231
|
+
}
|
|
1232
|
+
</select>
|
|
1233
|
+
</div>
|
|
1234
|
+
<button
|
|
1235
|
+
type="submit"
|
|
1236
|
+
class="inline-flex items-center px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700"
|
|
1237
|
+
>
|
|
1238
|
+
<ui-icon name="funnel" class="h-4 w-4 mr-1" />
|
|
1239
|
+
Filter
|
|
1240
|
+
</button>
|
|
1241
|
+
</form>
|
|
1242
|
+
|
|
1243
|
+
@if (error()) {
|
|
1244
|
+
<div class="bg-red-50 text-red-700 p-4 rounded-md">{{ error() }}</div>
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
<!-- Results info -->
|
|
1248
|
+
<div class="flex justify-between items-center">
|
|
1249
|
+
<p class="text-sm text-gray-500">
|
|
1250
|
+
@if (totalCount() > 0) {
|
|
1251
|
+
Showing {{ (currentPage() - 1) * pageSize + 1 }} -
|
|
1252
|
+
{{ Math.min(currentPage() * pageSize, totalCount()) }} of
|
|
1253
|
+
{{ totalCount().toLocaleString() }} logs
|
|
1254
|
+
} @else {
|
|
1255
|
+
No logs found
|
|
1256
|
+
}
|
|
1257
|
+
</p>
|
|
1258
|
+
@if (liveTail()) {
|
|
1259
|
+
<span class="inline-flex items-center text-sm text-green-600">
|
|
1260
|
+
<span class="h-2 w-2 bg-green-500 rounded-full mr-2 animate-pulse"></span>
|
|
1261
|
+
Live updating...
|
|
1262
|
+
</span>
|
|
1263
|
+
}
|
|
1264
|
+
</div>
|
|
1265
|
+
|
|
1266
|
+
<!-- Log Viewer -->
|
|
1267
|
+
<div
|
|
1268
|
+
class="rounded-lg overflow-hidden font-mono text-sm"
|
|
1269
|
+
[style.backgroundColor]="darkMode() ? '#000' : '#fff'"
|
|
1270
|
+
[style.color]="darkMode() ? '#f3f4f6' : '#111827'"
|
|
1271
|
+
[style.border]="darkMode() ? 'none' : '1px solid #e5e7eb'"
|
|
1272
|
+
>
|
|
1273
|
+
<div class="max-h-[600px] overflow-y-auto">
|
|
1274
|
+
@if (loading()) {
|
|
1275
|
+
<div class="p-8 text-center" [style.color]="darkMode() ? '#9ca3af' : '#6b7280'">Loading logs...</div>
|
|
1276
|
+
} @else if (logs().length === 0) {
|
|
1277
|
+
<div class="p-8 text-center" [style.color]="darkMode() ? '#9ca3af' : '#6b7280'">No logs found</div>
|
|
1278
|
+
} @else {
|
|
1279
|
+
@for (log of logs(); track log.id) {
|
|
1280
|
+
<div
|
|
1281
|
+
[style.borderBottom]="darkMode() ? '1px solid #1f2937' : '1px solid #e5e7eb'"
|
|
1282
|
+
[style.backgroundColor]="'transparent'"
|
|
1283
|
+
class="hover-row"
|
|
1284
|
+
>
|
|
1285
|
+
<div
|
|
1286
|
+
class="flex items-start p-2 cursor-pointer"
|
|
1287
|
+
(click)="toggleExpand(log.id)"
|
|
1288
|
+
>
|
|
1289
|
+
<div class="w-5 flex-shrink-0 mt-0.5">
|
|
1290
|
+
@if (hasDetails(log)) {
|
|
1291
|
+
@if (expandedLogs().has(log.id)) {
|
|
1292
|
+
<ui-icon name="chevron-down" class="h-4 w-4" [style.color]="darkMode() ? '#6b7280' : '#6b7280'" />
|
|
1293
|
+
} @else {
|
|
1294
|
+
<ui-icon name="chevron-right" class="h-4 w-4" [style.color]="darkMode() ? '#6b7280' : '#6b7280'" />
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
</div>
|
|
1298
|
+
<span class="w-44 flex-shrink-0" [style.color]="darkMode() ? '#9ca3af' : '#6b7280'">
|
|
1299
|
+
{{ formatDate(log.timestamp) }}
|
|
1300
|
+
</span>
|
|
1301
|
+
<span
|
|
1302
|
+
class="px-2 py-0.5 rounded text-xs font-medium w-16 text-center flex-shrink-0"
|
|
1303
|
+
[ngStyle]="getLevelStyles(log.level)"
|
|
1304
|
+
>
|
|
1305
|
+
{{ getLevelLabel(log.level) }}
|
|
1306
|
+
</span>
|
|
1307
|
+
<span class="w-32 flex-shrink-0 ml-2 truncate" [style.color]="darkMode() ? '#e879f9' : '#9333ea'">
|
|
1308
|
+
{{ log.category }}
|
|
1309
|
+
</span>
|
|
1310
|
+
<span class="ml-2 flex-1 break-all" [style.color]="darkMode() ? '#fff' : '#1f2937'">
|
|
1311
|
+
{{ log.message }}
|
|
1312
|
+
</span>
|
|
1313
|
+
</div>
|
|
1314
|
+
@if (expandedLogs().has(log.id) && hasDetails(log)) {
|
|
1315
|
+
<div class="px-8 pb-4 space-y-3 text-sm" [style.color]="darkMode() ? '#e5e7eb' : '#374151'">
|
|
1316
|
+
<!-- Context info row -->
|
|
1317
|
+
<div class="flex flex-wrap gap-4 text-xs">
|
|
1318
|
+
@if (log.requestMethod || log.requestPath) {
|
|
1319
|
+
<div class="flex items-center gap-1">
|
|
1320
|
+
<span [style.color]="darkMode() ? '#6b7280' : '#9ca3af'">Request:</span>
|
|
1321
|
+
<span class="font-semibold" [style.color]="darkMode() ? '#22d3ee' : '#0891b2'">{{ log.requestMethod }}</span>
|
|
1322
|
+
<span [style.color]="darkMode() ? '#22d3ee' : '#0891b2'">{{ log.requestPath }}</span>
|
|
1323
|
+
</div>
|
|
1324
|
+
}
|
|
1325
|
+
@if (log.userId) {
|
|
1326
|
+
<div class="flex items-center gap-1">
|
|
1327
|
+
<span [style.color]="darkMode() ? '#6b7280' : '#9ca3af'">User:</span>
|
|
1328
|
+
<span [style.color]="darkMode() ? '#00ff88' : '#16a34a'">{{ log.userId }}</span>
|
|
1329
|
+
</div>
|
|
1330
|
+
}
|
|
1331
|
+
@if (log.clientId) {
|
|
1332
|
+
<div class="flex items-center gap-1">
|
|
1333
|
+
<span [style.color]="darkMode() ? '#6b7280' : '#9ca3af'">Client:</span>
|
|
1334
|
+
<span [style.color]="darkMode() ? '#fbbf24' : '#d97706'">{{ log.clientId }}</span>
|
|
1335
|
+
</div>
|
|
1336
|
+
}
|
|
1337
|
+
@if (log.machineName) {
|
|
1338
|
+
<div class="flex items-center gap-1">
|
|
1339
|
+
<span [style.color]="darkMode() ? '#6b7280' : '#9ca3af'">Machine:</span>
|
|
1340
|
+
<span [style.color]="darkMode() ? '#d1d5db' : '#4b5563'">{{ log.machineName }}</span>
|
|
1341
|
+
</div>
|
|
1342
|
+
}
|
|
1343
|
+
</div>
|
|
1344
|
+
|
|
1345
|
+
<!-- Trace info -->
|
|
1346
|
+
@if (log.traceId) {
|
|
1347
|
+
<div class="flex items-center gap-2 text-xs">
|
|
1348
|
+
<span [style.color]="darkMode() ? '#6b7280' : '#9ca3af'">Trace:</span>
|
|
1349
|
+
<button
|
|
1350
|
+
(click)="navigateToTrace(log.traceId!); $event.stopPropagation()"
|
|
1351
|
+
class="font-mono hover:underline"
|
|
1352
|
+
[style.color]="darkMode() ? '#38bdf8' : '#2563eb'"
|
|
1353
|
+
>
|
|
1354
|
+
{{ log.traceId }}
|
|
1355
|
+
</button>
|
|
1356
|
+
@if (log.spanId) {
|
|
1357
|
+
<span [style.color]="darkMode() ? '#6b7280' : '#9ca3af'">Span:</span>
|
|
1358
|
+
<span class="font-mono" [style.color]="darkMode() ? '#9ca3af' : '#6b7280'">{{ log.spanId }}</span>
|
|
1359
|
+
}
|
|
1360
|
+
</div>
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
<!-- Properties -->
|
|
1364
|
+
@if (log.properties && hasProperties(log.properties)) {
|
|
1365
|
+
<div>
|
|
1366
|
+
<span class="text-xs" [style.color]="darkMode() ? '#6b7280' : '#9ca3af'">Properties:</span>
|
|
1367
|
+
<pre
|
|
1368
|
+
class="mt-1 text-xs p-3 rounded border overflow-x-auto"
|
|
1369
|
+
[style.color]="darkMode() ? '#00ff88' : '#15803d'"
|
|
1370
|
+
[style.backgroundColor]="darkMode() ? '#000' : '#f9fafb'"
|
|
1371
|
+
[style.borderColor]="darkMode() ? '#1f2937' : '#e5e7eb'"
|
|
1372
|
+
>{{ formatJson(log.properties) }}</pre>
|
|
1373
|
+
</div>
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
<!-- Exception details -->
|
|
1377
|
+
@if (log.exception || log.exceptionType) {
|
|
1378
|
+
<div>
|
|
1379
|
+
<div class="flex items-center gap-2 text-xs mb-1">
|
|
1380
|
+
<span class="font-semibold" [style.color]="darkMode() ? '#f87171' : '#dc2626'">Exception:</span>
|
|
1381
|
+
@if (log.exceptionType) {
|
|
1382
|
+
<span class="font-mono" [style.color]="darkMode() ? '#fca5a5' : '#dc2626'">{{ log.exceptionType }}</span>
|
|
1383
|
+
}
|
|
1384
|
+
</div>
|
|
1385
|
+
@if (log.exception) {
|
|
1386
|
+
<pre
|
|
1387
|
+
class="text-xs p-3 rounded border overflow-x-auto whitespace-pre-wrap"
|
|
1388
|
+
[style.color]="darkMode() ? '#fca5a5' : '#b91c1c'"
|
|
1389
|
+
[style.backgroundColor]="darkMode() ? 'rgba(127, 29, 29, 0.3)' : '#fef2f2'"
|
|
1390
|
+
[style.borderColor]="darkMode() ? '#7f1d1d' : '#fecaca'"
|
|
1391
|
+
>{{ log.exception }}</pre>
|
|
1392
|
+
}
|
|
1393
|
+
</div>
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
<!-- Stack trace -->
|
|
1397
|
+
@if (log.stackTrace) {
|
|
1398
|
+
<div>
|
|
1399
|
+
<span class="text-xs" [style.color]="darkMode() ? '#6b7280' : '#9ca3af'">Stack Trace:</span>
|
|
1400
|
+
<pre
|
|
1401
|
+
class="mt-1 text-xs p-3 rounded border overflow-x-auto whitespace-pre-wrap max-h-64 overflow-y-auto"
|
|
1402
|
+
[style.color]="darkMode() ? '#fdba74' : '#c2410c'"
|
|
1403
|
+
[style.backgroundColor]="darkMode() ? '#000' : '#f9fafb'"
|
|
1404
|
+
[style.borderColor]="darkMode() ? '#1f2937' : '#e5e7eb'"
|
|
1405
|
+
>{{ log.stackTrace }}</pre>
|
|
1406
|
+
</div>
|
|
1407
|
+
}
|
|
1408
|
+
</div>
|
|
1409
|
+
}
|
|
1410
|
+
</div>
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
</div>
|
|
1414
|
+
</div>
|
|
1415
|
+
|
|
1416
|
+
<!-- Pagination -->
|
|
1417
|
+
@if (totalPages() > 1) {
|
|
1418
|
+
<div class="flex justify-center items-center gap-2">
|
|
1419
|
+
<button
|
|
1420
|
+
(click)="goToPage(currentPage() - 1)"
|
|
1421
|
+
[disabled]="currentPage() === 1"
|
|
1422
|
+
class="px-3 py-1 border rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
|
1423
|
+
>
|
|
1424
|
+
Previous
|
|
1425
|
+
</button>
|
|
1426
|
+
<span class="text-sm text-gray-600">
|
|
1427
|
+
Page {{ currentPage() }} of {{ totalPages() }}
|
|
1428
|
+
</span>
|
|
1429
|
+
<button
|
|
1430
|
+
(click)="goToPage(currentPage() + 1)"
|
|
1431
|
+
[disabled]="currentPage() === totalPages()"
|
|
1432
|
+
class="px-3 py-1 border rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
|
1433
|
+
>
|
|
1434
|
+
Next
|
|
1435
|
+
</button>
|
|
1436
|
+
</div>
|
|
1437
|
+
}
|
|
1438
|
+
</div>
|
|
1439
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: IconComponent, selector: "ui-icon", inputs: ["name", "strokeWidth"] }] });
|
|
1440
|
+
}
|
|
1441
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: TelemetryLogsComponent, decorators: [{
|
|
1442
|
+
type: Component,
|
|
1443
|
+
args: [{
|
|
1444
|
+
selector: 'lib-telemetry-logs',
|
|
1445
|
+
standalone: true,
|
|
1446
|
+
imports: [CommonModule, FormsModule, IconComponent],
|
|
1447
|
+
template: `
|
|
1448
|
+
<div class="space-y-6">
|
|
1449
|
+
<!-- Header -->
|
|
1450
|
+
<div class="flex justify-between items-center">
|
|
1451
|
+
<div>
|
|
1452
|
+
<div class="flex items-center gap-2">
|
|
1453
|
+
<h1 class="text-2xl font-semibold text-gray-900">Application Logs</h1>
|
|
1454
|
+
@if (backend(); as b) {
|
|
1455
|
+
<span
|
|
1456
|
+
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"
|
|
1457
|
+
[class.bg-violet-100]="b === 'loki'"
|
|
1458
|
+
[class.text-violet-700]="b === 'loki'"
|
|
1459
|
+
[class.bg-gray-100]="b !== 'loki'"
|
|
1460
|
+
[class.text-gray-600]="b !== 'loki'"
|
|
1461
|
+
[title]="'Reading from ' + b"
|
|
1462
|
+
>
|
|
1463
|
+
{{ b }}
|
|
1464
|
+
</span>
|
|
1465
|
+
}
|
|
1466
|
+
</div>
|
|
1467
|
+
<p class="text-gray-500 mt-1">Search and filter logs from all services</p>
|
|
1468
|
+
</div>
|
|
1469
|
+
<div class="flex items-center gap-2">
|
|
1470
|
+
<!-- Theme toggle -->
|
|
1471
|
+
<button
|
|
1472
|
+
(click)="toggleTheme()"
|
|
1473
|
+
class="inline-flex items-center px-3 py-2 rounded-md text-sm bg-gray-100 text-gray-700 hover:bg-gray-200"
|
|
1474
|
+
[title]="darkMode() ? 'Switch to light mode' : 'Switch to dark mode'"
|
|
1475
|
+
>
|
|
1476
|
+
@if (darkMode()) {
|
|
1477
|
+
<ui-icon name="sun" class="h-4 w-4" />
|
|
1478
|
+
} @else {
|
|
1479
|
+
<ui-icon name="moon" class="h-4 w-4" />
|
|
1480
|
+
}
|
|
1481
|
+
</button>
|
|
1482
|
+
<button
|
|
1483
|
+
(click)="toggleLiveTail()"
|
|
1484
|
+
class="inline-flex items-center px-3 py-2 rounded-md text-sm"
|
|
1485
|
+
[class.bg-green-600]="liveTail()"
|
|
1486
|
+
[class.text-white]="liveTail()"
|
|
1487
|
+
[class.bg-gray-100]="!liveTail()"
|
|
1488
|
+
[class.text-gray-700]="!liveTail()"
|
|
1489
|
+
[class.hover:bg-gray-200]="!liveTail()"
|
|
1490
|
+
>
|
|
1491
|
+
@if (liveTail()) {
|
|
1492
|
+
<ui-icon name="stop-circle" class="h-4 w-4 mr-1" />
|
|
1493
|
+
Stop Live Tail
|
|
1494
|
+
} @else {
|
|
1495
|
+
<ui-icon name="play-circle" class="h-4 w-4 mr-1" />
|
|
1496
|
+
Live Tail
|
|
1497
|
+
}
|
|
1498
|
+
</button>
|
|
1499
|
+
<button
|
|
1500
|
+
(click)="loadLogs()"
|
|
1501
|
+
[disabled]="loading()"
|
|
1502
|
+
class="inline-flex items-center px-3 py-2 bg-gray-100 rounded-md hover:bg-gray-200 text-sm disabled:opacity-50"
|
|
1503
|
+
>
|
|
1504
|
+
<svg
|
|
1505
|
+
class="h-4 w-4 mr-1"
|
|
1506
|
+
[class.animate-spin]="loading()"
|
|
1507
|
+
fill="none"
|
|
1508
|
+
viewBox="0 0 24 24"
|
|
1509
|
+
stroke="currentColor"
|
|
1510
|
+
>
|
|
1511
|
+
<path
|
|
1512
|
+
stroke-linecap="round"
|
|
1513
|
+
stroke-linejoin="round"
|
|
1514
|
+
stroke-width="2"
|
|
1515
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
1516
|
+
/>
|
|
1517
|
+
</svg>
|
|
1518
|
+
Refresh
|
|
1519
|
+
</button>
|
|
1520
|
+
</div>
|
|
1521
|
+
</div>
|
|
1522
|
+
|
|
1523
|
+
<!-- Filters -->
|
|
1524
|
+
<form (ngSubmit)="handleSearch()" class="flex gap-4 items-end">
|
|
1525
|
+
<div class="flex-1">
|
|
1526
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Search</label>
|
|
1527
|
+
<div class="relative">
|
|
1528
|
+
<ui-icon name="search" class="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
|
1529
|
+
<input
|
|
1530
|
+
type="text"
|
|
1531
|
+
[(ngModel)]="searchTerm"
|
|
1532
|
+
name="searchTerm"
|
|
1533
|
+
placeholder="Search logs..."
|
|
1534
|
+
class="w-full pl-10 pr-4 py-2 border rounded-md"
|
|
1535
|
+
/>
|
|
1536
|
+
</div>
|
|
1537
|
+
</div>
|
|
1538
|
+
<div class="w-40">
|
|
1539
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Level</label>
|
|
1540
|
+
<select
|
|
1541
|
+
[(ngModel)]="level"
|
|
1542
|
+
name="level"
|
|
1543
|
+
class="w-full border rounded-md px-3 py-2"
|
|
1544
|
+
>
|
|
1545
|
+
<option value="">All Levels</option>
|
|
1546
|
+
<option value="trace">Trace</option>
|
|
1547
|
+
<option value="debug">Debug</option>
|
|
1548
|
+
<option value="info">Info</option>
|
|
1549
|
+
<option value="warn">Warning</option>
|
|
1550
|
+
<option value="error">Error</option>
|
|
1551
|
+
<option value="fatal">Fatal</option>
|
|
1552
|
+
</select>
|
|
1553
|
+
</div>
|
|
1554
|
+
<div class="w-48">
|
|
1555
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Category</label>
|
|
1556
|
+
<select
|
|
1557
|
+
[(ngModel)]="category"
|
|
1558
|
+
name="category"
|
|
1559
|
+
class="w-full border rounded-md px-3 py-2"
|
|
1560
|
+
>
|
|
1561
|
+
<option value="">All Categories</option>
|
|
1562
|
+
@for (cat of categories(); track cat) {
|
|
1563
|
+
<option [value]="cat">{{ cat }}</option>
|
|
1564
|
+
}
|
|
1565
|
+
</select>
|
|
1566
|
+
</div>
|
|
1567
|
+
<button
|
|
1568
|
+
type="submit"
|
|
1569
|
+
class="inline-flex items-center px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700"
|
|
1570
|
+
>
|
|
1571
|
+
<ui-icon name="funnel" class="h-4 w-4 mr-1" />
|
|
1572
|
+
Filter
|
|
1573
|
+
</button>
|
|
1574
|
+
</form>
|
|
1575
|
+
|
|
1576
|
+
@if (error()) {
|
|
1577
|
+
<div class="bg-red-50 text-red-700 p-4 rounded-md">{{ error() }}</div>
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
<!-- Results info -->
|
|
1581
|
+
<div class="flex justify-between items-center">
|
|
1582
|
+
<p class="text-sm text-gray-500">
|
|
1583
|
+
@if (totalCount() > 0) {
|
|
1584
|
+
Showing {{ (currentPage() - 1) * pageSize + 1 }} -
|
|
1585
|
+
{{ Math.min(currentPage() * pageSize, totalCount()) }} of
|
|
1586
|
+
{{ totalCount().toLocaleString() }} logs
|
|
1587
|
+
} @else {
|
|
1588
|
+
No logs found
|
|
1589
|
+
}
|
|
1590
|
+
</p>
|
|
1591
|
+
@if (liveTail()) {
|
|
1592
|
+
<span class="inline-flex items-center text-sm text-green-600">
|
|
1593
|
+
<span class="h-2 w-2 bg-green-500 rounded-full mr-2 animate-pulse"></span>
|
|
1594
|
+
Live updating...
|
|
1595
|
+
</span>
|
|
1596
|
+
}
|
|
1597
|
+
</div>
|
|
1598
|
+
|
|
1599
|
+
<!-- Log Viewer -->
|
|
1600
|
+
<div
|
|
1601
|
+
class="rounded-lg overflow-hidden font-mono text-sm"
|
|
1602
|
+
[style.backgroundColor]="darkMode() ? '#000' : '#fff'"
|
|
1603
|
+
[style.color]="darkMode() ? '#f3f4f6' : '#111827'"
|
|
1604
|
+
[style.border]="darkMode() ? 'none' : '1px solid #e5e7eb'"
|
|
1605
|
+
>
|
|
1606
|
+
<div class="max-h-[600px] overflow-y-auto">
|
|
1607
|
+
@if (loading()) {
|
|
1608
|
+
<div class="p-8 text-center" [style.color]="darkMode() ? '#9ca3af' : '#6b7280'">Loading logs...</div>
|
|
1609
|
+
} @else if (logs().length === 0) {
|
|
1610
|
+
<div class="p-8 text-center" [style.color]="darkMode() ? '#9ca3af' : '#6b7280'">No logs found</div>
|
|
1611
|
+
} @else {
|
|
1612
|
+
@for (log of logs(); track log.id) {
|
|
1613
|
+
<div
|
|
1614
|
+
[style.borderBottom]="darkMode() ? '1px solid #1f2937' : '1px solid #e5e7eb'"
|
|
1615
|
+
[style.backgroundColor]="'transparent'"
|
|
1616
|
+
class="hover-row"
|
|
1617
|
+
>
|
|
1618
|
+
<div
|
|
1619
|
+
class="flex items-start p-2 cursor-pointer"
|
|
1620
|
+
(click)="toggleExpand(log.id)"
|
|
1621
|
+
>
|
|
1622
|
+
<div class="w-5 flex-shrink-0 mt-0.5">
|
|
1623
|
+
@if (hasDetails(log)) {
|
|
1624
|
+
@if (expandedLogs().has(log.id)) {
|
|
1625
|
+
<ui-icon name="chevron-down" class="h-4 w-4" [style.color]="darkMode() ? '#6b7280' : '#6b7280'" />
|
|
1626
|
+
} @else {
|
|
1627
|
+
<ui-icon name="chevron-right" class="h-4 w-4" [style.color]="darkMode() ? '#6b7280' : '#6b7280'" />
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
</div>
|
|
1631
|
+
<span class="w-44 flex-shrink-0" [style.color]="darkMode() ? '#9ca3af' : '#6b7280'">
|
|
1632
|
+
{{ formatDate(log.timestamp) }}
|
|
1633
|
+
</span>
|
|
1634
|
+
<span
|
|
1635
|
+
class="px-2 py-0.5 rounded text-xs font-medium w-16 text-center flex-shrink-0"
|
|
1636
|
+
[ngStyle]="getLevelStyles(log.level)"
|
|
1637
|
+
>
|
|
1638
|
+
{{ getLevelLabel(log.level) }}
|
|
1639
|
+
</span>
|
|
1640
|
+
<span class="w-32 flex-shrink-0 ml-2 truncate" [style.color]="darkMode() ? '#e879f9' : '#9333ea'">
|
|
1641
|
+
{{ log.category }}
|
|
1642
|
+
</span>
|
|
1643
|
+
<span class="ml-2 flex-1 break-all" [style.color]="darkMode() ? '#fff' : '#1f2937'">
|
|
1644
|
+
{{ log.message }}
|
|
1645
|
+
</span>
|
|
1646
|
+
</div>
|
|
1647
|
+
@if (expandedLogs().has(log.id) && hasDetails(log)) {
|
|
1648
|
+
<div class="px-8 pb-4 space-y-3 text-sm" [style.color]="darkMode() ? '#e5e7eb' : '#374151'">
|
|
1649
|
+
<!-- Context info row -->
|
|
1650
|
+
<div class="flex flex-wrap gap-4 text-xs">
|
|
1651
|
+
@if (log.requestMethod || log.requestPath) {
|
|
1652
|
+
<div class="flex items-center gap-1">
|
|
1653
|
+
<span [style.color]="darkMode() ? '#6b7280' : '#9ca3af'">Request:</span>
|
|
1654
|
+
<span class="font-semibold" [style.color]="darkMode() ? '#22d3ee' : '#0891b2'">{{ log.requestMethod }}</span>
|
|
1655
|
+
<span [style.color]="darkMode() ? '#22d3ee' : '#0891b2'">{{ log.requestPath }}</span>
|
|
1656
|
+
</div>
|
|
1657
|
+
}
|
|
1658
|
+
@if (log.userId) {
|
|
1659
|
+
<div class="flex items-center gap-1">
|
|
1660
|
+
<span [style.color]="darkMode() ? '#6b7280' : '#9ca3af'">User:</span>
|
|
1661
|
+
<span [style.color]="darkMode() ? '#00ff88' : '#16a34a'">{{ log.userId }}</span>
|
|
1662
|
+
</div>
|
|
1663
|
+
}
|
|
1664
|
+
@if (log.clientId) {
|
|
1665
|
+
<div class="flex items-center gap-1">
|
|
1666
|
+
<span [style.color]="darkMode() ? '#6b7280' : '#9ca3af'">Client:</span>
|
|
1667
|
+
<span [style.color]="darkMode() ? '#fbbf24' : '#d97706'">{{ log.clientId }}</span>
|
|
1668
|
+
</div>
|
|
1669
|
+
}
|
|
1670
|
+
@if (log.machineName) {
|
|
1671
|
+
<div class="flex items-center gap-1">
|
|
1672
|
+
<span [style.color]="darkMode() ? '#6b7280' : '#9ca3af'">Machine:</span>
|
|
1673
|
+
<span [style.color]="darkMode() ? '#d1d5db' : '#4b5563'">{{ log.machineName }}</span>
|
|
1674
|
+
</div>
|
|
1675
|
+
}
|
|
1676
|
+
</div>
|
|
1677
|
+
|
|
1678
|
+
<!-- Trace info -->
|
|
1679
|
+
@if (log.traceId) {
|
|
1680
|
+
<div class="flex items-center gap-2 text-xs">
|
|
1681
|
+
<span [style.color]="darkMode() ? '#6b7280' : '#9ca3af'">Trace:</span>
|
|
1682
|
+
<button
|
|
1683
|
+
(click)="navigateToTrace(log.traceId!); $event.stopPropagation()"
|
|
1684
|
+
class="font-mono hover:underline"
|
|
1685
|
+
[style.color]="darkMode() ? '#38bdf8' : '#2563eb'"
|
|
1686
|
+
>
|
|
1687
|
+
{{ log.traceId }}
|
|
1688
|
+
</button>
|
|
1689
|
+
@if (log.spanId) {
|
|
1690
|
+
<span [style.color]="darkMode() ? '#6b7280' : '#9ca3af'">Span:</span>
|
|
1691
|
+
<span class="font-mono" [style.color]="darkMode() ? '#9ca3af' : '#6b7280'">{{ log.spanId }}</span>
|
|
1692
|
+
}
|
|
1693
|
+
</div>
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
<!-- Properties -->
|
|
1697
|
+
@if (log.properties && hasProperties(log.properties)) {
|
|
1698
|
+
<div>
|
|
1699
|
+
<span class="text-xs" [style.color]="darkMode() ? '#6b7280' : '#9ca3af'">Properties:</span>
|
|
1700
|
+
<pre
|
|
1701
|
+
class="mt-1 text-xs p-3 rounded border overflow-x-auto"
|
|
1702
|
+
[style.color]="darkMode() ? '#00ff88' : '#15803d'"
|
|
1703
|
+
[style.backgroundColor]="darkMode() ? '#000' : '#f9fafb'"
|
|
1704
|
+
[style.borderColor]="darkMode() ? '#1f2937' : '#e5e7eb'"
|
|
1705
|
+
>{{ formatJson(log.properties) }}</pre>
|
|
1706
|
+
</div>
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
<!-- Exception details -->
|
|
1710
|
+
@if (log.exception || log.exceptionType) {
|
|
1711
|
+
<div>
|
|
1712
|
+
<div class="flex items-center gap-2 text-xs mb-1">
|
|
1713
|
+
<span class="font-semibold" [style.color]="darkMode() ? '#f87171' : '#dc2626'">Exception:</span>
|
|
1714
|
+
@if (log.exceptionType) {
|
|
1715
|
+
<span class="font-mono" [style.color]="darkMode() ? '#fca5a5' : '#dc2626'">{{ log.exceptionType }}</span>
|
|
1716
|
+
}
|
|
1717
|
+
</div>
|
|
1718
|
+
@if (log.exception) {
|
|
1719
|
+
<pre
|
|
1720
|
+
class="text-xs p-3 rounded border overflow-x-auto whitespace-pre-wrap"
|
|
1721
|
+
[style.color]="darkMode() ? '#fca5a5' : '#b91c1c'"
|
|
1722
|
+
[style.backgroundColor]="darkMode() ? 'rgba(127, 29, 29, 0.3)' : '#fef2f2'"
|
|
1723
|
+
[style.borderColor]="darkMode() ? '#7f1d1d' : '#fecaca'"
|
|
1724
|
+
>{{ log.exception }}</pre>
|
|
1725
|
+
}
|
|
1726
|
+
</div>
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
<!-- Stack trace -->
|
|
1730
|
+
@if (log.stackTrace) {
|
|
1731
|
+
<div>
|
|
1732
|
+
<span class="text-xs" [style.color]="darkMode() ? '#6b7280' : '#9ca3af'">Stack Trace:</span>
|
|
1733
|
+
<pre
|
|
1734
|
+
class="mt-1 text-xs p-3 rounded border overflow-x-auto whitespace-pre-wrap max-h-64 overflow-y-auto"
|
|
1735
|
+
[style.color]="darkMode() ? '#fdba74' : '#c2410c'"
|
|
1736
|
+
[style.backgroundColor]="darkMode() ? '#000' : '#f9fafb'"
|
|
1737
|
+
[style.borderColor]="darkMode() ? '#1f2937' : '#e5e7eb'"
|
|
1738
|
+
>{{ log.stackTrace }}</pre>
|
|
1739
|
+
</div>
|
|
1740
|
+
}
|
|
1741
|
+
</div>
|
|
1742
|
+
}
|
|
1743
|
+
</div>
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
</div>
|
|
1747
|
+
</div>
|
|
1748
|
+
|
|
1749
|
+
<!-- Pagination -->
|
|
1750
|
+
@if (totalPages() > 1) {
|
|
1751
|
+
<div class="flex justify-center items-center gap-2">
|
|
1752
|
+
<button
|
|
1753
|
+
(click)="goToPage(currentPage() - 1)"
|
|
1754
|
+
[disabled]="currentPage() === 1"
|
|
1755
|
+
class="px-3 py-1 border rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
|
1756
|
+
>
|
|
1757
|
+
Previous
|
|
1758
|
+
</button>
|
|
1759
|
+
<span class="text-sm text-gray-600">
|
|
1760
|
+
Page {{ currentPage() }} of {{ totalPages() }}
|
|
1761
|
+
</span>
|
|
1762
|
+
<button
|
|
1763
|
+
(click)="goToPage(currentPage() + 1)"
|
|
1764
|
+
[disabled]="currentPage() === totalPages()"
|
|
1765
|
+
class="px-3 py-1 border rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
|
1766
|
+
>
|
|
1767
|
+
Next
|
|
1768
|
+
</button>
|
|
1769
|
+
</div>
|
|
1770
|
+
}
|
|
1771
|
+
</div>
|
|
1772
|
+
`,
|
|
1773
|
+
}]
|
|
1774
|
+
}] });
|
|
1775
|
+
|
|
1776
|
+
var telemetryLogs_component = /*#__PURE__*/Object.freeze({
|
|
1777
|
+
__proto__: null,
|
|
1778
|
+
TelemetryLogsComponent: TelemetryLogsComponent
|
|
1779
|
+
});
|
|
1780
|
+
|
|
1781
|
+
class TelemetryTracesComponent {
|
|
1782
|
+
telemetryService = inject(TelemetryService);
|
|
1783
|
+
router = inject(Router);
|
|
1784
|
+
route = inject(ActivatedRoute);
|
|
1785
|
+
traces = signal([], ...(ngDevMode ? [{ debugName: "traces" }] : /* istanbul ignore next */ []));
|
|
1786
|
+
selectedTrace = signal(null, ...(ngDevMode ? [{ debugName: "selectedTrace" }] : /* istanbul ignore next */ []));
|
|
1787
|
+
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
1788
|
+
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
1789
|
+
totalCount = signal(0, ...(ngDevMode ? [{ debugName: "totalCount" }] : /* istanbul ignore next */ []));
|
|
1790
|
+
expandedSpans = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedSpans" }] : /* istanbul ignore next */ []));
|
|
1791
|
+
/**
|
|
1792
|
+
* Trace backend configured-state.
|
|
1793
|
+
* - `null`: status check still in flight (initial load)
|
|
1794
|
+
* - `true`: Tempo (or other) backend is wired; render search/list
|
|
1795
|
+
* - `false`: no backend; render the "not configured" notice instead
|
|
1796
|
+
*/
|
|
1797
|
+
backendConfigured = signal(null, ...(ngDevMode ? [{ debugName: "backendConfigured" }] : /* istanbul ignore next */ []));
|
|
1798
|
+
searchTerm = '';
|
|
1799
|
+
currentPage = signal(1, ...(ngDevMode ? [{ debugName: "currentPage" }] : /* istanbul ignore next */ []));
|
|
1800
|
+
pageSize = 20;
|
|
1801
|
+
totalPages = computed(() => Math.ceil(this.totalCount() / this.pageSize) || 1, ...(ngDevMode ? [{ debugName: "totalPages" }] : /* istanbul ignore next */ []));
|
|
1802
|
+
traceId = signal(null, ...(ngDevMode ? [{ debugName: "traceId" }] : /* istanbul ignore next */ []));
|
|
1803
|
+
// Expose Math and formatDuration for template
|
|
1804
|
+
Math = Math;
|
|
1805
|
+
formatDuration = formatDuration;
|
|
1806
|
+
ngOnInit() {
|
|
1807
|
+
// Check trace backend status first so we can render the "not configured"
|
|
1808
|
+
// banner without firing a doomed search query.
|
|
1809
|
+
this.telemetryService.getTraceBackendStatus().subscribe({
|
|
1810
|
+
next: (status) => {
|
|
1811
|
+
this.backendConfigured.set(status.isConfigured);
|
|
1812
|
+
if (status.isConfigured) {
|
|
1813
|
+
this.bindRoute();
|
|
1814
|
+
}
|
|
1815
|
+
},
|
|
1816
|
+
error: () => {
|
|
1817
|
+
// Status endpoint missing or failing — assume configured so we still
|
|
1818
|
+
// attempt the load. A real failure surfaces via the search error path.
|
|
1819
|
+
this.backendConfigured.set(true);
|
|
1820
|
+
this.bindRoute();
|
|
1821
|
+
},
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
bindRoute() {
|
|
1825
|
+
this.route.paramMap.subscribe((params) => {
|
|
1826
|
+
const id = params.get('traceId');
|
|
1827
|
+
this.traceId.set(id);
|
|
1828
|
+
if (id) {
|
|
1829
|
+
this.loadTrace(id);
|
|
1830
|
+
}
|
|
1831
|
+
else {
|
|
1832
|
+
this.loadTraces();
|
|
1833
|
+
}
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
loadTraces() {
|
|
1837
|
+
this.loading.set(true);
|
|
1838
|
+
this.error.set(null);
|
|
1839
|
+
this.telemetryService
|
|
1840
|
+
.getTraces({
|
|
1841
|
+
searchTerm: this.searchTerm || undefined,
|
|
1842
|
+
page: this.currentPage(),
|
|
1843
|
+
pageSize: this.pageSize,
|
|
1844
|
+
})
|
|
1845
|
+
.subscribe({
|
|
1846
|
+
next: (result) => {
|
|
1847
|
+
this.traces.set(result.items);
|
|
1848
|
+
this.totalCount.set(result.totalCount);
|
|
1849
|
+
this.loading.set(false);
|
|
1850
|
+
},
|
|
1851
|
+
error: (err) => {
|
|
1852
|
+
this.error.set('Failed to load traces');
|
|
1853
|
+
console.error(err);
|
|
1854
|
+
this.loading.set(false);
|
|
1855
|
+
},
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
loadTrace(id) {
|
|
1859
|
+
this.loading.set(true);
|
|
1860
|
+
this.error.set(null);
|
|
1861
|
+
this.telemetryService.getTrace(id).subscribe({
|
|
1862
|
+
next: (trace) => {
|
|
1863
|
+
this.selectedTrace.set(trace);
|
|
1864
|
+
// Expand root span by default
|
|
1865
|
+
this.expandedSpans.set(new Set([trace.rootSpan.spanId]));
|
|
1866
|
+
this.loading.set(false);
|
|
1867
|
+
},
|
|
1868
|
+
error: (err) => {
|
|
1869
|
+
this.error.set('Failed to load trace details');
|
|
1870
|
+
console.error(err);
|
|
1871
|
+
this.loading.set(false);
|
|
1872
|
+
},
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
handleSearch() {
|
|
1876
|
+
this.currentPage.set(1);
|
|
1877
|
+
this.loadTraces();
|
|
1878
|
+
}
|
|
1879
|
+
selectTrace(id) {
|
|
1880
|
+
this.router.navigate(['/telemetry/traces', id]);
|
|
1881
|
+
}
|
|
1882
|
+
handleBack() {
|
|
1883
|
+
this.selectedTrace.set(null);
|
|
1884
|
+
this.router.navigate(['/telemetry/traces']);
|
|
1885
|
+
}
|
|
1886
|
+
goToPage(page) {
|
|
1887
|
+
if (page >= 1 && page <= this.totalPages()) {
|
|
1888
|
+
this.currentPage.set(page);
|
|
1889
|
+
this.loadTraces();
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
toggleSpanExpand(spanId) {
|
|
1893
|
+
this.expandedSpans.update((set) => {
|
|
1894
|
+
const newSet = new Set(set);
|
|
1895
|
+
if (newSet.has(spanId)) {
|
|
1896
|
+
newSet.delete(spanId);
|
|
1897
|
+
}
|
|
1898
|
+
else {
|
|
1899
|
+
newSet.add(spanId);
|
|
1900
|
+
}
|
|
1901
|
+
return newSet;
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
getStatusColor(status) {
|
|
1905
|
+
return TRACE_STATUS_CONFIG[status].textColor;
|
|
1906
|
+
}
|
|
1907
|
+
formatDate(dateString) {
|
|
1908
|
+
return new Date(dateString).toLocaleString();
|
|
1909
|
+
}
|
|
1910
|
+
getSpanOffset(span, trace) {
|
|
1911
|
+
const traceStart = new Date(trace.startTime).getTime();
|
|
1912
|
+
const spanStart = new Date(span.startTime).getTime();
|
|
1913
|
+
return ((spanStart - traceStart) / trace.duration) * 100;
|
|
1914
|
+
}
|
|
1915
|
+
getSpanWidth(span, trace) {
|
|
1916
|
+
return Math.max((span.duration / trace.duration) * 100, 1);
|
|
1917
|
+
}
|
|
1918
|
+
/**
|
|
1919
|
+
* Build a flattened tree of spans for rendering
|
|
1920
|
+
*/
|
|
1921
|
+
getSpanTree(trace) {
|
|
1922
|
+
const result = [];
|
|
1923
|
+
const spansByParent = new Map();
|
|
1924
|
+
// Group spans by parent
|
|
1925
|
+
for (const span of trace.spans) {
|
|
1926
|
+
const children = spansByParent.get(span.parentSpanId) || [];
|
|
1927
|
+
children.push(span);
|
|
1928
|
+
spansByParent.set(span.parentSpanId, children);
|
|
1929
|
+
}
|
|
1930
|
+
// Recursive function to traverse
|
|
1931
|
+
const traverse = (span, depth) => {
|
|
1932
|
+
const children = spansByParent.get(span.spanId) || [];
|
|
1933
|
+
const hasChildren = children.length > 0;
|
|
1934
|
+
const isExpanded = this.expandedSpans().has(span.spanId);
|
|
1935
|
+
result.push({ ...span, depth, hasChildren });
|
|
1936
|
+
if (hasChildren && isExpanded) {
|
|
1937
|
+
for (const child of children) {
|
|
1938
|
+
traverse(child, depth + 1);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
};
|
|
1942
|
+
traverse(trace.rootSpan, 0);
|
|
1943
|
+
return result;
|
|
1944
|
+
}
|
|
1945
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: TelemetryTracesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1946
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: TelemetryTracesComponent, isStandalone: true, selector: "lib-telemetry-traces", ngImport: i0, template: `
|
|
1947
|
+
<div class="space-y-6">
|
|
1948
|
+
@if (traceId()) {
|
|
1949
|
+
<!-- Trace Detail View -->
|
|
1950
|
+
<div>
|
|
1951
|
+
<button
|
|
1952
|
+
(click)="handleBack()"
|
|
1953
|
+
class="inline-flex items-center text-gray-600 hover:text-gray-900 mb-4"
|
|
1954
|
+
>
|
|
1955
|
+
<ui-icon name="arrow-left" class="h-4 w-4 mr-1" />
|
|
1956
|
+
Back to Traces
|
|
1957
|
+
</button>
|
|
1958
|
+
<h1 class="text-2xl font-semibold text-gray-900">Trace Details</h1>
|
|
1959
|
+
</div>
|
|
1960
|
+
|
|
1961
|
+
@if (error()) {
|
|
1962
|
+
<div class="bg-red-50 text-red-700 p-4 rounded-md">{{ error() }}</div>
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
@if (loading()) {
|
|
1966
|
+
<div class="flex items-center justify-center p-12">
|
|
1967
|
+
<svg
|
|
1968
|
+
class="h-8 w-8 animate-spin text-gray-400"
|
|
1969
|
+
fill="none"
|
|
1970
|
+
viewBox="0 0 24 24"
|
|
1971
|
+
stroke="currentColor"
|
|
1972
|
+
>
|
|
1973
|
+
<path
|
|
1974
|
+
stroke-linecap="round"
|
|
1975
|
+
stroke-linejoin="round"
|
|
1976
|
+
stroke-width="2"
|
|
1977
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
1978
|
+
/>
|
|
1979
|
+
</svg>
|
|
1980
|
+
</div>
|
|
1981
|
+
} @else if (selectedTrace()) {
|
|
1982
|
+
<!-- Trace Viewer -->
|
|
1983
|
+
<div class="bg-white rounded-lg shadow overflow-hidden">
|
|
1984
|
+
<div class="p-4 border-b bg-gray-50">
|
|
1985
|
+
<div class="flex items-center justify-between">
|
|
1986
|
+
<div>
|
|
1987
|
+
<h3 class="font-medium text-gray-900">{{ selectedTrace()!.rootSpan.operationName }}</h3>
|
|
1988
|
+
<p class="text-sm text-gray-500">
|
|
1989
|
+
Trace ID: <code class="bg-gray-100 px-1 rounded">{{ selectedTrace()!.traceId }}</code>
|
|
1990
|
+
</p>
|
|
1991
|
+
</div>
|
|
1992
|
+
<div class="text-right">
|
|
1993
|
+
<p class="text-lg font-semibold">{{ formatDuration(selectedTrace()!.duration) }}</p>
|
|
1994
|
+
<p class="text-sm text-gray-500">
|
|
1995
|
+
{{ formatDate(selectedTrace()!.startTime) }}
|
|
1996
|
+
</p>
|
|
1997
|
+
</div>
|
|
1998
|
+
</div>
|
|
1999
|
+
</div>
|
|
2000
|
+
<div class="divide-y">
|
|
2001
|
+
@for (span of getSpanTree(selectedTrace()!); track span.spanId) {
|
|
2002
|
+
<div
|
|
2003
|
+
class="flex items-center py-2 px-2 hover:bg-gray-50 cursor-pointer border-b"
|
|
2004
|
+
[style.padding-left.px]="span.depth * 24 + 8"
|
|
2005
|
+
(click)="toggleSpanExpand(span.spanId)"
|
|
2006
|
+
>
|
|
2007
|
+
<div class="w-5 flex-shrink-0">
|
|
2008
|
+
@if (span.hasChildren) {
|
|
2009
|
+
@if (expandedSpans().has(span.spanId)) {
|
|
2010
|
+
<ui-icon name="chevron-down" class="h-4 w-4 text-gray-400" />
|
|
2011
|
+
} @else {
|
|
2012
|
+
<ui-icon name="chevron-right" class="h-4 w-4 text-gray-400" />
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
</div>
|
|
2016
|
+
<!-- Status Icon -->
|
|
2017
|
+
@if (span.status === 'ok') {
|
|
2018
|
+
<ui-icon name="check-circle" [class]="'h-4 w-4 mr-2 ' + getStatusColor(span.status)" />
|
|
2019
|
+
} @else if (span.status === 'error') {
|
|
2020
|
+
<ui-icon name="x-circle" [class]="'h-4 w-4 mr-2 ' + getStatusColor(span.status)" />
|
|
2021
|
+
} @else {
|
|
2022
|
+
<svg [class]="'h-4 w-4 mr-2 ' + getStatusColor(span.status)" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
2023
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
2024
|
+
</svg>
|
|
2025
|
+
}
|
|
2026
|
+
<div class="flex-1 min-w-0">
|
|
2027
|
+
<div class="flex items-center gap-2">
|
|
2028
|
+
<span class="font-medium text-gray-900 truncate">{{ span.operationName }}</span>
|
|
2029
|
+
<span class="text-xs text-gray-500">{{ span.serviceName }}</span>
|
|
2030
|
+
</div>
|
|
2031
|
+
</div>
|
|
2032
|
+
<!-- Timing bar -->
|
|
2033
|
+
<div class="w-64 flex-shrink-0 mx-4">
|
|
2034
|
+
<div class="h-2 bg-gray-100 rounded relative">
|
|
2035
|
+
<div
|
|
2036
|
+
class="absolute h-full rounded"
|
|
2037
|
+
[class.bg-red-400]="span.status === 'error'"
|
|
2038
|
+
[class.bg-blue-400]="span.status !== 'error'"
|
|
2039
|
+
[style.left.%]="getSpanOffset(span, selectedTrace()!)"
|
|
2040
|
+
[style.width.%]="getSpanWidth(span, selectedTrace()!)"
|
|
2041
|
+
></div>
|
|
2042
|
+
</div>
|
|
2043
|
+
</div>
|
|
2044
|
+
<span class="text-sm text-gray-600 w-20 text-right">
|
|
2045
|
+
{{ formatDuration(span.duration) }}
|
|
2046
|
+
</span>
|
|
2047
|
+
</div>
|
|
2048
|
+
}
|
|
2049
|
+
</div>
|
|
2050
|
+
</div>
|
|
2051
|
+
} @else {
|
|
2052
|
+
<div class="text-center text-gray-500 p-12">Trace not found</div>
|
|
2053
|
+
}
|
|
2054
|
+
} @else {
|
|
2055
|
+
<!-- Trace List View -->
|
|
2056
|
+
<div class="flex justify-between items-center">
|
|
2057
|
+
<div>
|
|
2058
|
+
<h1 class="text-2xl font-semibold text-gray-900">Distributed Traces</h1>
|
|
2059
|
+
<p class="text-gray-500 mt-1">
|
|
2060
|
+
Track requests across services and identify performance bottlenecks
|
|
2061
|
+
</p>
|
|
2062
|
+
</div>
|
|
2063
|
+
<button
|
|
2064
|
+
(click)="loadTraces()"
|
|
2065
|
+
[disabled]="loading() || backendConfigured() === false"
|
|
2066
|
+
class="inline-flex items-center px-3 py-2 bg-gray-100 rounded-md hover:bg-gray-200 text-sm disabled:opacity-50"
|
|
2067
|
+
>
|
|
2068
|
+
<svg
|
|
2069
|
+
class="h-4 w-4 mr-1"
|
|
2070
|
+
[class.animate-spin]="loading()"
|
|
2071
|
+
fill="none"
|
|
2072
|
+
viewBox="0 0 24 24"
|
|
2073
|
+
stroke="currentColor"
|
|
2074
|
+
>
|
|
2075
|
+
<path
|
|
2076
|
+
stroke-linecap="round"
|
|
2077
|
+
stroke-linejoin="round"
|
|
2078
|
+
stroke-width="2"
|
|
2079
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
2080
|
+
/>
|
|
2081
|
+
</svg>
|
|
2082
|
+
Refresh
|
|
2083
|
+
</button>
|
|
2084
|
+
</div>
|
|
2085
|
+
|
|
2086
|
+
@if (backendConfigured() === false) {
|
|
2087
|
+
<!-- Trace backend not configured (e.g. dev/staging without Tempo) -->
|
|
2088
|
+
<div class="bg-amber-50 border border-amber-200 rounded-md p-6">
|
|
2089
|
+
<div class="flex items-start gap-3">
|
|
2090
|
+
<ui-icon name="information-circle" class="h-5 w-5 text-amber-600 flex-shrink-0 mt-0.5" />
|
|
2091
|
+
<div>
|
|
2092
|
+
<h3 class="font-medium text-amber-900">Distributed tracing isn't configured for this environment</h3>
|
|
2093
|
+
<p class="text-sm text-amber-800 mt-1">
|
|
2094
|
+
Spans are still being captured and exported via OTLP, but no
|
|
2095
|
+
query backend (e.g. Grafana Tempo) is wired up here so traces
|
|
2096
|
+
can't be displayed. Set
|
|
2097
|
+
<code class="bg-amber-100 px-1 rounded text-xs">Oluso__Telemetry__TempoEndpoint</code>
|
|
2098
|
+
on the API service to point at your Tempo HTTP API
|
|
2099
|
+
(typically <code class="bg-amber-100 px-1 rounded text-xs">http://tempo:3200</code>)
|
|
2100
|
+
to enable this view.
|
|
2101
|
+
</p>
|
|
2102
|
+
</div>
|
|
2103
|
+
</div>
|
|
2104
|
+
</div>
|
|
2105
|
+
} @else {
|
|
2106
|
+
<!-- Search -->
|
|
2107
|
+
<form (ngSubmit)="handleSearch()" class="flex gap-4">
|
|
2108
|
+
<div class="flex-1 relative">
|
|
2109
|
+
<ui-icon name="search" class="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
|
2110
|
+
<input
|
|
2111
|
+
type="text"
|
|
2112
|
+
[(ngModel)]="searchTerm"
|
|
2113
|
+
name="searchTerm"
|
|
2114
|
+
placeholder="Search by operation name, trace ID, or service..."
|
|
2115
|
+
class="w-full pl-10 pr-4 py-2 border rounded-md"
|
|
2116
|
+
/>
|
|
2117
|
+
</div>
|
|
2118
|
+
<button
|
|
2119
|
+
type="submit"
|
|
2120
|
+
class="px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700"
|
|
2121
|
+
>
|
|
2122
|
+
Search
|
|
2123
|
+
</button>
|
|
2124
|
+
</form>
|
|
2125
|
+
|
|
2126
|
+
@if (error()) {
|
|
2127
|
+
<div class="bg-red-50 text-red-700 p-4 rounded-md">{{ error() }}</div>
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
<!-- Results info -->
|
|
2131
|
+
<div>
|
|
2132
|
+
<p class="text-sm text-gray-500">
|
|
2133
|
+
@if (totalCount() > 0) {
|
|
2134
|
+
Showing {{ (currentPage() - 1) * pageSize + 1 }} -
|
|
2135
|
+
{{ Math.min(currentPage() * pageSize, totalCount()) }} of
|
|
2136
|
+
{{ totalCount().toLocaleString() }} traces
|
|
2137
|
+
} @else {
|
|
2138
|
+
No traces found
|
|
2139
|
+
}
|
|
2140
|
+
</p>
|
|
2141
|
+
</div>
|
|
2142
|
+
|
|
2143
|
+
<!-- Trace list -->
|
|
2144
|
+
@if (loading() && traces().length === 0) {
|
|
2145
|
+
<div class="flex items-center justify-center p-12">
|
|
2146
|
+
<svg
|
|
2147
|
+
class="h-8 w-8 animate-spin text-gray-400"
|
|
2148
|
+
fill="none"
|
|
2149
|
+
viewBox="0 0 24 24"
|
|
2150
|
+
stroke="currentColor"
|
|
2151
|
+
>
|
|
2152
|
+
<path
|
|
2153
|
+
stroke-linecap="round"
|
|
2154
|
+
stroke-linejoin="round"
|
|
2155
|
+
stroke-width="2"
|
|
2156
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
2157
|
+
/>
|
|
2158
|
+
</svg>
|
|
2159
|
+
</div>
|
|
2160
|
+
} @else {
|
|
2161
|
+
<div class="bg-white rounded-lg shadow overflow-hidden">
|
|
2162
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
2163
|
+
<thead class="bg-gray-50">
|
|
2164
|
+
<tr>
|
|
2165
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Operation</th>
|
|
2166
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Service</th>
|
|
2167
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
|
2168
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Duration</th>
|
|
2169
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Time</th>
|
|
2170
|
+
</tr>
|
|
2171
|
+
</thead>
|
|
2172
|
+
<tbody class="divide-y divide-gray-200">
|
|
2173
|
+
@if (traces().length === 0) {
|
|
2174
|
+
<tr>
|
|
2175
|
+
<td colspan="5" class="px-4 py-8 text-center text-gray-500">No traces found</td>
|
|
2176
|
+
</tr>
|
|
2177
|
+
} @else {
|
|
2178
|
+
@for (trace of traces(); track trace.traceId) {
|
|
2179
|
+
<tr
|
|
2180
|
+
class="hover:bg-gray-50 cursor-pointer"
|
|
2181
|
+
(click)="selectTrace(trace.traceId)"
|
|
2182
|
+
>
|
|
2183
|
+
<td class="px-4 py-3">
|
|
2184
|
+
<span class="font-medium text-primary-600 hover:underline">
|
|
2185
|
+
{{ trace.rootSpan.operationName }}
|
|
2186
|
+
</span>
|
|
2187
|
+
</td>
|
|
2188
|
+
<td class="px-4 py-3 text-sm text-gray-600">{{ trace.serviceName }}</td>
|
|
2189
|
+
<td class="px-4 py-3">
|
|
2190
|
+
@if (trace.status === 'ok') {
|
|
2191
|
+
<ui-icon name="check-circle" [class]="'h-5 w-5 ' + getStatusColor(trace.status)" />
|
|
2192
|
+
} @else if (trace.status === 'error') {
|
|
2193
|
+
<ui-icon name="x-circle" [class]="'h-5 w-5 ' + getStatusColor(trace.status)" />
|
|
2194
|
+
} @else {
|
|
2195
|
+
<svg [class]="'h-5 w-5 ' + getStatusColor(trace.status)" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
2196
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
2197
|
+
</svg>
|
|
2198
|
+
}
|
|
2199
|
+
</td>
|
|
2200
|
+
<td class="px-4 py-3 text-sm">{{ formatDuration(trace.duration) }}</td>
|
|
2201
|
+
<td class="px-4 py-3 text-sm text-gray-500">{{ formatDate(trace.startTime) }}</td>
|
|
2202
|
+
</tr>
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
</tbody>
|
|
2206
|
+
</table>
|
|
2207
|
+
</div>
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
<!-- Pagination -->
|
|
2211
|
+
@if (totalPages() > 1) {
|
|
2212
|
+
<div class="flex justify-center items-center gap-2">
|
|
2213
|
+
<button
|
|
2214
|
+
(click)="goToPage(currentPage() - 1)"
|
|
2215
|
+
[disabled]="currentPage() === 1"
|
|
2216
|
+
class="px-3 py-1 border rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
|
2217
|
+
>
|
|
2218
|
+
Previous
|
|
2219
|
+
</button>
|
|
2220
|
+
<span class="text-sm text-gray-600">
|
|
2221
|
+
Page {{ currentPage() }} of {{ totalPages() }}
|
|
2222
|
+
</span>
|
|
2223
|
+
<button
|
|
2224
|
+
(click)="goToPage(currentPage() + 1)"
|
|
2225
|
+
[disabled]="currentPage() === totalPages()"
|
|
2226
|
+
class="px-3 py-1 border rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
|
2227
|
+
>
|
|
2228
|
+
Next
|
|
2229
|
+
</button>
|
|
2230
|
+
</div>
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
</div>
|
|
2235
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: IconComponent, selector: "ui-icon", inputs: ["name", "strokeWidth"] }] });
|
|
2236
|
+
}
|
|
2237
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: TelemetryTracesComponent, decorators: [{
|
|
2238
|
+
type: Component,
|
|
2239
|
+
args: [{
|
|
2240
|
+
selector: 'lib-telemetry-traces',
|
|
2241
|
+
standalone: true,
|
|
2242
|
+
imports: [CommonModule, FormsModule, IconComponent],
|
|
2243
|
+
template: `
|
|
2244
|
+
<div class="space-y-6">
|
|
2245
|
+
@if (traceId()) {
|
|
2246
|
+
<!-- Trace Detail View -->
|
|
2247
|
+
<div>
|
|
2248
|
+
<button
|
|
2249
|
+
(click)="handleBack()"
|
|
2250
|
+
class="inline-flex items-center text-gray-600 hover:text-gray-900 mb-4"
|
|
2251
|
+
>
|
|
2252
|
+
<ui-icon name="arrow-left" class="h-4 w-4 mr-1" />
|
|
2253
|
+
Back to Traces
|
|
2254
|
+
</button>
|
|
2255
|
+
<h1 class="text-2xl font-semibold text-gray-900">Trace Details</h1>
|
|
2256
|
+
</div>
|
|
2257
|
+
|
|
2258
|
+
@if (error()) {
|
|
2259
|
+
<div class="bg-red-50 text-red-700 p-4 rounded-md">{{ error() }}</div>
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
@if (loading()) {
|
|
2263
|
+
<div class="flex items-center justify-center p-12">
|
|
2264
|
+
<svg
|
|
2265
|
+
class="h-8 w-8 animate-spin text-gray-400"
|
|
2266
|
+
fill="none"
|
|
2267
|
+
viewBox="0 0 24 24"
|
|
2268
|
+
stroke="currentColor"
|
|
2269
|
+
>
|
|
2270
|
+
<path
|
|
2271
|
+
stroke-linecap="round"
|
|
2272
|
+
stroke-linejoin="round"
|
|
2273
|
+
stroke-width="2"
|
|
2274
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
2275
|
+
/>
|
|
2276
|
+
</svg>
|
|
2277
|
+
</div>
|
|
2278
|
+
} @else if (selectedTrace()) {
|
|
2279
|
+
<!-- Trace Viewer -->
|
|
2280
|
+
<div class="bg-white rounded-lg shadow overflow-hidden">
|
|
2281
|
+
<div class="p-4 border-b bg-gray-50">
|
|
2282
|
+
<div class="flex items-center justify-between">
|
|
2283
|
+
<div>
|
|
2284
|
+
<h3 class="font-medium text-gray-900">{{ selectedTrace()!.rootSpan.operationName }}</h3>
|
|
2285
|
+
<p class="text-sm text-gray-500">
|
|
2286
|
+
Trace ID: <code class="bg-gray-100 px-1 rounded">{{ selectedTrace()!.traceId }}</code>
|
|
2287
|
+
</p>
|
|
2288
|
+
</div>
|
|
2289
|
+
<div class="text-right">
|
|
2290
|
+
<p class="text-lg font-semibold">{{ formatDuration(selectedTrace()!.duration) }}</p>
|
|
2291
|
+
<p class="text-sm text-gray-500">
|
|
2292
|
+
{{ formatDate(selectedTrace()!.startTime) }}
|
|
2293
|
+
</p>
|
|
2294
|
+
</div>
|
|
2295
|
+
</div>
|
|
2296
|
+
</div>
|
|
2297
|
+
<div class="divide-y">
|
|
2298
|
+
@for (span of getSpanTree(selectedTrace()!); track span.spanId) {
|
|
2299
|
+
<div
|
|
2300
|
+
class="flex items-center py-2 px-2 hover:bg-gray-50 cursor-pointer border-b"
|
|
2301
|
+
[style.padding-left.px]="span.depth * 24 + 8"
|
|
2302
|
+
(click)="toggleSpanExpand(span.spanId)"
|
|
2303
|
+
>
|
|
2304
|
+
<div class="w-5 flex-shrink-0">
|
|
2305
|
+
@if (span.hasChildren) {
|
|
2306
|
+
@if (expandedSpans().has(span.spanId)) {
|
|
2307
|
+
<ui-icon name="chevron-down" class="h-4 w-4 text-gray-400" />
|
|
2308
|
+
} @else {
|
|
2309
|
+
<ui-icon name="chevron-right" class="h-4 w-4 text-gray-400" />
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
</div>
|
|
2313
|
+
<!-- Status Icon -->
|
|
2314
|
+
@if (span.status === 'ok') {
|
|
2315
|
+
<ui-icon name="check-circle" [class]="'h-4 w-4 mr-2 ' + getStatusColor(span.status)" />
|
|
2316
|
+
} @else if (span.status === 'error') {
|
|
2317
|
+
<ui-icon name="x-circle" [class]="'h-4 w-4 mr-2 ' + getStatusColor(span.status)" />
|
|
2318
|
+
} @else {
|
|
2319
|
+
<svg [class]="'h-4 w-4 mr-2 ' + getStatusColor(span.status)" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
2320
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
2321
|
+
</svg>
|
|
2322
|
+
}
|
|
2323
|
+
<div class="flex-1 min-w-0">
|
|
2324
|
+
<div class="flex items-center gap-2">
|
|
2325
|
+
<span class="font-medium text-gray-900 truncate">{{ span.operationName }}</span>
|
|
2326
|
+
<span class="text-xs text-gray-500">{{ span.serviceName }}</span>
|
|
2327
|
+
</div>
|
|
2328
|
+
</div>
|
|
2329
|
+
<!-- Timing bar -->
|
|
2330
|
+
<div class="w-64 flex-shrink-0 mx-4">
|
|
2331
|
+
<div class="h-2 bg-gray-100 rounded relative">
|
|
2332
|
+
<div
|
|
2333
|
+
class="absolute h-full rounded"
|
|
2334
|
+
[class.bg-red-400]="span.status === 'error'"
|
|
2335
|
+
[class.bg-blue-400]="span.status !== 'error'"
|
|
2336
|
+
[style.left.%]="getSpanOffset(span, selectedTrace()!)"
|
|
2337
|
+
[style.width.%]="getSpanWidth(span, selectedTrace()!)"
|
|
2338
|
+
></div>
|
|
2339
|
+
</div>
|
|
2340
|
+
</div>
|
|
2341
|
+
<span class="text-sm text-gray-600 w-20 text-right">
|
|
2342
|
+
{{ formatDuration(span.duration) }}
|
|
2343
|
+
</span>
|
|
2344
|
+
</div>
|
|
2345
|
+
}
|
|
2346
|
+
</div>
|
|
2347
|
+
</div>
|
|
2348
|
+
} @else {
|
|
2349
|
+
<div class="text-center text-gray-500 p-12">Trace not found</div>
|
|
2350
|
+
}
|
|
2351
|
+
} @else {
|
|
2352
|
+
<!-- Trace List View -->
|
|
2353
|
+
<div class="flex justify-between items-center">
|
|
2354
|
+
<div>
|
|
2355
|
+
<h1 class="text-2xl font-semibold text-gray-900">Distributed Traces</h1>
|
|
2356
|
+
<p class="text-gray-500 mt-1">
|
|
2357
|
+
Track requests across services and identify performance bottlenecks
|
|
2358
|
+
</p>
|
|
2359
|
+
</div>
|
|
2360
|
+
<button
|
|
2361
|
+
(click)="loadTraces()"
|
|
2362
|
+
[disabled]="loading() || backendConfigured() === false"
|
|
2363
|
+
class="inline-flex items-center px-3 py-2 bg-gray-100 rounded-md hover:bg-gray-200 text-sm disabled:opacity-50"
|
|
2364
|
+
>
|
|
2365
|
+
<svg
|
|
2366
|
+
class="h-4 w-4 mr-1"
|
|
2367
|
+
[class.animate-spin]="loading()"
|
|
2368
|
+
fill="none"
|
|
2369
|
+
viewBox="0 0 24 24"
|
|
2370
|
+
stroke="currentColor"
|
|
2371
|
+
>
|
|
2372
|
+
<path
|
|
2373
|
+
stroke-linecap="round"
|
|
2374
|
+
stroke-linejoin="round"
|
|
2375
|
+
stroke-width="2"
|
|
2376
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
2377
|
+
/>
|
|
2378
|
+
</svg>
|
|
2379
|
+
Refresh
|
|
2380
|
+
</button>
|
|
2381
|
+
</div>
|
|
2382
|
+
|
|
2383
|
+
@if (backendConfigured() === false) {
|
|
2384
|
+
<!-- Trace backend not configured (e.g. dev/staging without Tempo) -->
|
|
2385
|
+
<div class="bg-amber-50 border border-amber-200 rounded-md p-6">
|
|
2386
|
+
<div class="flex items-start gap-3">
|
|
2387
|
+
<ui-icon name="information-circle" class="h-5 w-5 text-amber-600 flex-shrink-0 mt-0.5" />
|
|
2388
|
+
<div>
|
|
2389
|
+
<h3 class="font-medium text-amber-900">Distributed tracing isn't configured for this environment</h3>
|
|
2390
|
+
<p class="text-sm text-amber-800 mt-1">
|
|
2391
|
+
Spans are still being captured and exported via OTLP, but no
|
|
2392
|
+
query backend (e.g. Grafana Tempo) is wired up here so traces
|
|
2393
|
+
can't be displayed. Set
|
|
2394
|
+
<code class="bg-amber-100 px-1 rounded text-xs">Oluso__Telemetry__TempoEndpoint</code>
|
|
2395
|
+
on the API service to point at your Tempo HTTP API
|
|
2396
|
+
(typically <code class="bg-amber-100 px-1 rounded text-xs">http://tempo:3200</code>)
|
|
2397
|
+
to enable this view.
|
|
2398
|
+
</p>
|
|
2399
|
+
</div>
|
|
2400
|
+
</div>
|
|
2401
|
+
</div>
|
|
2402
|
+
} @else {
|
|
2403
|
+
<!-- Search -->
|
|
2404
|
+
<form (ngSubmit)="handleSearch()" class="flex gap-4">
|
|
2405
|
+
<div class="flex-1 relative">
|
|
2406
|
+
<ui-icon name="search" class="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
|
2407
|
+
<input
|
|
2408
|
+
type="text"
|
|
2409
|
+
[(ngModel)]="searchTerm"
|
|
2410
|
+
name="searchTerm"
|
|
2411
|
+
placeholder="Search by operation name, trace ID, or service..."
|
|
2412
|
+
class="w-full pl-10 pr-4 py-2 border rounded-md"
|
|
2413
|
+
/>
|
|
2414
|
+
</div>
|
|
2415
|
+
<button
|
|
2416
|
+
type="submit"
|
|
2417
|
+
class="px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700"
|
|
2418
|
+
>
|
|
2419
|
+
Search
|
|
2420
|
+
</button>
|
|
2421
|
+
</form>
|
|
2422
|
+
|
|
2423
|
+
@if (error()) {
|
|
2424
|
+
<div class="bg-red-50 text-red-700 p-4 rounded-md">{{ error() }}</div>
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
<!-- Results info -->
|
|
2428
|
+
<div>
|
|
2429
|
+
<p class="text-sm text-gray-500">
|
|
2430
|
+
@if (totalCount() > 0) {
|
|
2431
|
+
Showing {{ (currentPage() - 1) * pageSize + 1 }} -
|
|
2432
|
+
{{ Math.min(currentPage() * pageSize, totalCount()) }} of
|
|
2433
|
+
{{ totalCount().toLocaleString() }} traces
|
|
2434
|
+
} @else {
|
|
2435
|
+
No traces found
|
|
2436
|
+
}
|
|
2437
|
+
</p>
|
|
2438
|
+
</div>
|
|
2439
|
+
|
|
2440
|
+
<!-- Trace list -->
|
|
2441
|
+
@if (loading() && traces().length === 0) {
|
|
2442
|
+
<div class="flex items-center justify-center p-12">
|
|
2443
|
+
<svg
|
|
2444
|
+
class="h-8 w-8 animate-spin text-gray-400"
|
|
2445
|
+
fill="none"
|
|
2446
|
+
viewBox="0 0 24 24"
|
|
2447
|
+
stroke="currentColor"
|
|
2448
|
+
>
|
|
2449
|
+
<path
|
|
2450
|
+
stroke-linecap="round"
|
|
2451
|
+
stroke-linejoin="round"
|
|
2452
|
+
stroke-width="2"
|
|
2453
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
2454
|
+
/>
|
|
2455
|
+
</svg>
|
|
2456
|
+
</div>
|
|
2457
|
+
} @else {
|
|
2458
|
+
<div class="bg-white rounded-lg shadow overflow-hidden">
|
|
2459
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
2460
|
+
<thead class="bg-gray-50">
|
|
2461
|
+
<tr>
|
|
2462
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Operation</th>
|
|
2463
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Service</th>
|
|
2464
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
|
2465
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Duration</th>
|
|
2466
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Time</th>
|
|
2467
|
+
</tr>
|
|
2468
|
+
</thead>
|
|
2469
|
+
<tbody class="divide-y divide-gray-200">
|
|
2470
|
+
@if (traces().length === 0) {
|
|
2471
|
+
<tr>
|
|
2472
|
+
<td colspan="5" class="px-4 py-8 text-center text-gray-500">No traces found</td>
|
|
2473
|
+
</tr>
|
|
2474
|
+
} @else {
|
|
2475
|
+
@for (trace of traces(); track trace.traceId) {
|
|
2476
|
+
<tr
|
|
2477
|
+
class="hover:bg-gray-50 cursor-pointer"
|
|
2478
|
+
(click)="selectTrace(trace.traceId)"
|
|
2479
|
+
>
|
|
2480
|
+
<td class="px-4 py-3">
|
|
2481
|
+
<span class="font-medium text-primary-600 hover:underline">
|
|
2482
|
+
{{ trace.rootSpan.operationName }}
|
|
2483
|
+
</span>
|
|
2484
|
+
</td>
|
|
2485
|
+
<td class="px-4 py-3 text-sm text-gray-600">{{ trace.serviceName }}</td>
|
|
2486
|
+
<td class="px-4 py-3">
|
|
2487
|
+
@if (trace.status === 'ok') {
|
|
2488
|
+
<ui-icon name="check-circle" [class]="'h-5 w-5 ' + getStatusColor(trace.status)" />
|
|
2489
|
+
} @else if (trace.status === 'error') {
|
|
2490
|
+
<ui-icon name="x-circle" [class]="'h-5 w-5 ' + getStatusColor(trace.status)" />
|
|
2491
|
+
} @else {
|
|
2492
|
+
<svg [class]="'h-5 w-5 ' + getStatusColor(trace.status)" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
2493
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
2494
|
+
</svg>
|
|
2495
|
+
}
|
|
2496
|
+
</td>
|
|
2497
|
+
<td class="px-4 py-3 text-sm">{{ formatDuration(trace.duration) }}</td>
|
|
2498
|
+
<td class="px-4 py-3 text-sm text-gray-500">{{ formatDate(trace.startTime) }}</td>
|
|
2499
|
+
</tr>
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
</tbody>
|
|
2503
|
+
</table>
|
|
2504
|
+
</div>
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
<!-- Pagination -->
|
|
2508
|
+
@if (totalPages() > 1) {
|
|
2509
|
+
<div class="flex justify-center items-center gap-2">
|
|
2510
|
+
<button
|
|
2511
|
+
(click)="goToPage(currentPage() - 1)"
|
|
2512
|
+
[disabled]="currentPage() === 1"
|
|
2513
|
+
class="px-3 py-1 border rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
|
2514
|
+
>
|
|
2515
|
+
Previous
|
|
2516
|
+
</button>
|
|
2517
|
+
<span class="text-sm text-gray-600">
|
|
2518
|
+
Page {{ currentPage() }} of {{ totalPages() }}
|
|
2519
|
+
</span>
|
|
2520
|
+
<button
|
|
2521
|
+
(click)="goToPage(currentPage() + 1)"
|
|
2522
|
+
[disabled]="currentPage() === totalPages()"
|
|
2523
|
+
class="px-3 py-1 border rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
|
2524
|
+
>
|
|
2525
|
+
Next
|
|
2526
|
+
</button>
|
|
2527
|
+
</div>
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
</div>
|
|
2532
|
+
`,
|
|
2533
|
+
}]
|
|
2534
|
+
}] });
|
|
2535
|
+
|
|
2536
|
+
var telemetryTraces_component = /*#__PURE__*/Object.freeze({
|
|
2537
|
+
__proto__: null,
|
|
2538
|
+
TelemetryTracesComponent: TelemetryTracesComponent
|
|
2539
|
+
});
|
|
2540
|
+
|
|
2541
|
+
// Telemetry Plugin
|
|
2542
|
+
|
|
2543
|
+
/**
|
|
2544
|
+
* Generated bundle index. Do not edit.
|
|
2545
|
+
*/
|
|
2546
|
+
|
|
2547
|
+
export { LOG_LEVEL_CONFIG, TELEMETRY_PLUGIN, TELEMETRY_ROUTES, TRACE_STATUS_CONFIG, TelemetryDashboardComponent, TelemetryLogsComponent, TelemetryService, TelemetryTracesComponent, formatDuration, getIntervalForRange, getTelemetryNavItems };
|
|
2548
|
+
//# sourceMappingURL=syndew-oluso-plugin-telemetry.mjs.map
|