@syndew/oluso-plugin-telemetry 0.0.0-stage → 0.0.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