@momentumcms/plugins-otel 0.5.4 → 0.5.5

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,780 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __esm = (fn, res) => function __init() {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ };
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
+ var __decorateClass = (decorators, target, key, kind) => {
23
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
24
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
25
+ if (decorator = decorators[i])
26
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
27
+ if (kind && result)
28
+ __defProp(target, key, result);
29
+ return result;
30
+ };
31
+
32
+ // libs/plugins/otel/src/lib/dashboard/otel.service.ts
33
+ var import_common, import_core, DEFAULT_POLL_INTERVAL_MS, OtelService;
34
+ var init_otel_service = __esm({
35
+ "libs/plugins/otel/src/lib/dashboard/otel.service.ts"() {
36
+ "use strict";
37
+ import_common = require("@angular/common");
38
+ import_core = require("@angular/core");
39
+ DEFAULT_POLL_INTERVAL_MS = 5e3;
40
+ OtelService = class {
41
+ constructor() {
42
+ this.loading = (0, import_core.signal)(false);
43
+ this.error = (0, import_core.signal)(null);
44
+ this.summary = (0, import_core.signal)(null);
45
+ this.live = (0, import_core.signal)(false);
46
+ this.history = (0, import_core.signal)([]);
47
+ this.historyTotal = (0, import_core.signal)(0);
48
+ this.historyLoading = (0, import_core.signal)(false);
49
+ this.exporting = (0, import_core.signal)(false);
50
+ this.purging = (0, import_core.signal)(false);
51
+ this.document = (0, import_core.inject)(import_common.DOCUMENT);
52
+ this.window = this.document.defaultView;
53
+ this.pollTimer = null;
54
+ }
55
+ ngOnDestroy() {
56
+ this.stopPolling();
57
+ }
58
+ toggleLive(intervalMs = DEFAULT_POLL_INTERVAL_MS) {
59
+ if (this.live()) {
60
+ this.stopPolling();
61
+ } else {
62
+ this.startPolling(intervalMs);
63
+ }
64
+ }
65
+ async fetchSummary() {
66
+ this.loading.set(true);
67
+ this.error.set(null);
68
+ try {
69
+ const response = await fetch("/api/otel/summary");
70
+ if (!response.ok) {
71
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
72
+ }
73
+ const data = await response.json();
74
+ this.summary.set(data);
75
+ } catch (err) {
76
+ const message = err instanceof Error ? err.message : String(err);
77
+ this.error.set(message);
78
+ } finally {
79
+ this.loading.set(false);
80
+ }
81
+ }
82
+ async fetchHistory(from, to) {
83
+ this.historyLoading.set(true);
84
+ try {
85
+ const params = new URLSearchParams();
86
+ if (from)
87
+ params.set("from", from);
88
+ if (to)
89
+ params.set("to", to);
90
+ params.set("limit", "100");
91
+ const response = await fetch(`/api/otel/history?${params.toString()}`);
92
+ if (!response.ok) {
93
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
94
+ }
95
+ const data = await response.json();
96
+ this.history.set(Array.isArray(data.snapshots) ? data.snapshots : []);
97
+ this.historyTotal.set(typeof data.total === "number" ? data.total : 0);
98
+ } catch (err) {
99
+ const message = err instanceof Error ? err.message : String(err);
100
+ this.error.set(message);
101
+ } finally {
102
+ this.historyLoading.set(false);
103
+ }
104
+ }
105
+ exportCsv(from, to) {
106
+ this.exporting.set(true);
107
+ const params = new URLSearchParams();
108
+ if (from)
109
+ params.set("from", from);
110
+ if (to)
111
+ params.set("to", to);
112
+ const url = `/api/otel/export?${params.toString()}`;
113
+ const link = this.document.createElement("a");
114
+ link.href = url;
115
+ link.download = "";
116
+ link.style.display = "none";
117
+ this.document.body.appendChild(link);
118
+ link.click();
119
+ this.document.body.removeChild(link);
120
+ this.window?.setTimeout(() => this.exporting.set(false), 2e3);
121
+ }
122
+ async purgeHistory() {
123
+ this.purging.set(true);
124
+ try {
125
+ const response = await fetch("/api/otel/history", { method: "DELETE" });
126
+ if (!response.ok) {
127
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
128
+ }
129
+ this.history.set([]);
130
+ this.historyTotal.set(0);
131
+ } catch (err) {
132
+ const message = err instanceof Error ? err.message : String(err);
133
+ this.error.set(message);
134
+ } finally {
135
+ this.purging.set(false);
136
+ }
137
+ }
138
+ startPolling(intervalMs) {
139
+ this.stopPolling();
140
+ this.live.set(true);
141
+ this.pollTimer = this.window?.setInterval(() => void this.fetchSummary(), intervalMs) ?? null;
142
+ }
143
+ stopPolling() {
144
+ if (this.pollTimer != null) {
145
+ this.window?.clearInterval(this.pollTimer);
146
+ this.pollTimer = null;
147
+ }
148
+ this.live.set(false);
149
+ }
150
+ };
151
+ OtelService = __decorateClass([
152
+ (0, import_core.Injectable)({ providedIn: "root" })
153
+ ], OtelService);
154
+ }
155
+ });
156
+
157
+ // libs/plugins/otel/src/lib/dashboard/otel-dashboard.page.ts
158
+ var otel_dashboard_page_exports = {};
159
+ __export(otel_dashboard_page_exports, {
160
+ OtelDashboardPage: () => OtelDashboardPage
161
+ });
162
+ var import_core2, import_common2, import_ui, import_core3, import_outline, OtelDashboardPage;
163
+ var init_otel_dashboard_page = __esm({
164
+ "libs/plugins/otel/src/lib/dashboard/otel-dashboard.page.ts"() {
165
+ "use strict";
166
+ import_core2 = require("@angular/core");
167
+ import_common2 = require("@angular/common");
168
+ import_ui = require("@momentumcms/ui");
169
+ import_core3 = require("@ng-icons/core");
170
+ import_outline = require("@ng-icons/heroicons/outline");
171
+ init_otel_service();
172
+ OtelDashboardPage = class {
173
+ constructor() {
174
+ this.otel = (0, import_core2.inject)(OtelService);
175
+ this.platformId = (0, import_core2.inject)(import_core2.PLATFORM_ID);
176
+ this.window = (0, import_core2.inject)(import_common2.DOCUMENT).defaultView;
177
+ this.selectedRange = (0, import_core2.signal)("day");
178
+ this.confirmingPurge = (0, import_core2.signal)(false);
179
+ this.purgeResetTimer = null;
180
+ this.historyRanges = [
181
+ { key: "hour", label: "Last Hour" },
182
+ { key: "day", label: "Last 24h" },
183
+ { key: "week", label: "Last 7 Days" }
184
+ ];
185
+ }
186
+ ngOnInit() {
187
+ if (!(0, import_common2.isPlatformBrowser)(this.platformId))
188
+ return;
189
+ void this.refresh();
190
+ void this.loadHistory();
191
+ }
192
+ ngOnDestroy() {
193
+ if (this.otel.live()) {
194
+ this.otel.toggleLive();
195
+ }
196
+ if (this.purgeResetTimer != null) {
197
+ this.window?.clearTimeout(this.purgeResetTimer);
198
+ }
199
+ }
200
+ async refresh() {
201
+ await this.otel.fetchSummary();
202
+ }
203
+ selectRange(range) {
204
+ this.selectedRange.set(range);
205
+ void this.loadHistory();
206
+ }
207
+ exportCsv() {
208
+ const { from } = this.getTimeRange();
209
+ this.otel.exportCsv(from);
210
+ }
211
+ confirmPurge() {
212
+ if (this.purgeResetTimer != null) {
213
+ this.window?.clearTimeout(this.purgeResetTimer);
214
+ this.purgeResetTimer = null;
215
+ }
216
+ if (this.confirmingPurge()) {
217
+ this.confirmingPurge.set(false);
218
+ void this.otel.purgeHistory();
219
+ } else {
220
+ this.confirmingPurge.set(true);
221
+ this.purgeResetTimer = this.window?.setTimeout(() => {
222
+ this.confirmingPurge.set(false);
223
+ this.purgeResetTimer = null;
224
+ }, 3e3) ?? null;
225
+ }
226
+ }
227
+ formatUptime(seconds) {
228
+ if (seconds < 60)
229
+ return `${seconds}s`;
230
+ if (seconds < 3600)
231
+ return `${Math.floor(seconds / 60)}m`;
232
+ if (seconds < 86400)
233
+ return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m`;
234
+ return `${Math.floor(seconds / 86400)}d ${Math.floor(seconds % 86400 / 3600)}h`;
235
+ }
236
+ errorRate(s) {
237
+ if (s.requestMetrics.totalRequests === 0)
238
+ return "0";
239
+ return (s.requestMetrics.errorCount / s.requestMetrics.totalRequests * 100).toFixed(1);
240
+ }
241
+ methodEntries(s) {
242
+ return Object.entries(s.requestMetrics.byMethod).map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count);
243
+ }
244
+ statusEntries(s) {
245
+ return Object.entries(s.requestMetrics.byStatusCode).map(([name, count]) => ({ name, count })).sort((a, b) => Number(a.name) - Number(b.name));
246
+ }
247
+ statusVariant(status) {
248
+ const code = Number(status);
249
+ if (code >= 500)
250
+ return "destructive";
251
+ if (code >= 400)
252
+ return "warning";
253
+ if (code >= 200 && code < 300)
254
+ return "success";
255
+ return "secondary";
256
+ }
257
+ spanStatusVariant(span) {
258
+ return span.status === "ok" ? "success" : "destructive";
259
+ }
260
+ formatTime(timestamp) {
261
+ const now = Date.now();
262
+ const then = new Date(timestamp).getTime();
263
+ const diff = now - then;
264
+ if (diff < 6e4)
265
+ return "Just now";
266
+ if (diff < 36e5)
267
+ return `${Math.floor(diff / 6e4)}m ago`;
268
+ if (diff < 864e5)
269
+ return `${Math.floor(diff / 36e5)}h ago`;
270
+ return `${Math.floor(diff / 864e5)}d ago`;
271
+ }
272
+ formatSnapshotTime(timestamp) {
273
+ if (!timestamp)
274
+ return "\u2014";
275
+ const date = new Date(timestamp);
276
+ return date.toLocaleString(void 0, {
277
+ month: "short",
278
+ day: "numeric",
279
+ hour: "2-digit",
280
+ minute: "2-digit"
281
+ });
282
+ }
283
+ truncateId(id) {
284
+ if (!id || id.length <= 12)
285
+ return id || "\u2014";
286
+ return id.slice(0, 8) + "...";
287
+ }
288
+ async loadHistory() {
289
+ const { from } = this.getTimeRange();
290
+ await this.otel.fetchHistory(from);
291
+ }
292
+ getTimeRange() {
293
+ const now = Date.now();
294
+ const range = this.selectedRange();
295
+ let ms = 24 * 60 * 60 * 1e3;
296
+ if (range === "hour")
297
+ ms = 60 * 60 * 1e3;
298
+ else if (range === "week")
299
+ ms = 7 * 24 * 60 * 60 * 1e3;
300
+ return { from: new Date(now - ms).toISOString() };
301
+ }
302
+ };
303
+ OtelDashboardPage = __decorateClass([
304
+ (0, import_core2.Component)({
305
+ selector: "mcms-otel-dashboard",
306
+ imports: [
307
+ import_ui.Card,
308
+ import_ui.CardHeader,
309
+ import_ui.CardTitle,
310
+ import_ui.CardDescription,
311
+ import_ui.CardContent,
312
+ import_ui.Badge,
313
+ import_ui.Skeleton,
314
+ import_core3.NgIcon
315
+ ],
316
+ providers: [
317
+ (0, import_core3.provideIcons)({
318
+ heroSignal: import_outline.heroSignal,
319
+ heroArrowPath: import_outline.heroArrowPath,
320
+ heroClock: import_outline.heroClock,
321
+ heroServerStack: import_outline.heroServerStack,
322
+ heroChartBarSquare: import_outline.heroChartBarSquare,
323
+ heroCpuChip: import_outline.heroCpuChip,
324
+ heroDocumentText: import_outline.heroDocumentText,
325
+ heroEye: import_outline.heroEye,
326
+ heroArrowDownTray: import_outline.heroArrowDownTray,
327
+ heroTrash: import_outline.heroTrash
328
+ })
329
+ ],
330
+ changeDetection: import_core2.ChangeDetectionStrategy.OnPush,
331
+ host: { class: "block max-w-6xl" },
332
+ template: `
333
+ <header class="mb-10">
334
+ <div class="flex items-center justify-between">
335
+ <div>
336
+ <h1 class="text-4xl font-bold tracking-tight text-foreground">Observability</h1>
337
+ <p class="text-muted-foreground mt-3 text-lg">
338
+ System health, request metrics, and trace visibility
339
+ </p>
340
+ </div>
341
+ <div class="flex items-center gap-3">
342
+ <button
343
+ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md
344
+ transition-colors cursor-pointer
345
+ focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
346
+ [class]="otel.live()
347
+ ? 'bg-green-600 text-white hover:bg-green-700'
348
+ : 'border border-border bg-background text-foreground hover:bg-muted'"
349
+ (click)="otel.toggleLive()"
350
+ [attr.aria-label]="otel.live() ? 'Disable live updates' : 'Enable live updates (every 5s)'"
351
+ [attr.aria-pressed]="otel.live()"
352
+ >
353
+ <span
354
+ class="inline-block h-2 w-2 rounded-full"
355
+ [class]="otel.live() ? 'bg-white animate-pulse' : 'bg-muted-foreground'"
356
+ aria-hidden="true"
357
+ ></span>
358
+ Live
359
+ </button>
360
+ <button
361
+ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md
362
+ bg-primary text-primary-foreground hover:bg-primary/90 transition-colors cursor-pointer
363
+ focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary
364
+ disabled:opacity-50 disabled:cursor-not-allowed"
365
+ (click)="refresh()"
366
+ [attr.aria-label]="otel.loading() ? 'Refreshing observability data' : 'Refresh observability data'"
367
+ [disabled]="otel.loading()"
368
+ >
369
+ <ng-icon name="heroArrowPath" size="16" aria-hidden="true" />
370
+ Refresh
371
+ </button>
372
+ </div>
373
+ </div>
374
+ </header>
375
+
376
+ <div aria-live="polite" class="sr-only">
377
+ @if (otel.loading()) { Loading observability data... }
378
+ </div>
379
+
380
+ <!-- System Health -->
381
+ <section class="mb-10" aria-labelledby="system-health-heading">
382
+ <h2 id="system-health-heading" class="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">
383
+ System Health
384
+ </h2>
385
+ @if (otel.loading() && !otel.summary()) {
386
+ <div class="grid grid-cols-1 sm:grid-cols-3 gap-6" aria-busy="true" aria-label="Loading system health data">
387
+ @for (i of [1, 2, 3]; track i) {
388
+ <mcms-card>
389
+ <mcms-card-header>
390
+ <mcms-skeleton class="h-4 w-24" />
391
+ <mcms-skeleton class="h-8 w-16 mt-2" />
392
+ </mcms-card-header>
393
+ </mcms-card>
394
+ }
395
+ </div>
396
+ } @else if (otel.summary(); as s) {
397
+ <div class="grid grid-cols-1 sm:grid-cols-3 gap-6">
398
+ <mcms-card>
399
+ <mcms-card-header>
400
+ <div class="flex items-center justify-between">
401
+ <mcms-card-description>Uptime</mcms-card-description>
402
+ <ng-icon name="heroClock" class="text-muted-foreground" size="20" aria-hidden="true" />
403
+ </div>
404
+ <mcms-card-title>
405
+ <span class="text-3xl font-bold">{{ formatUptime(s.uptime) }}</span>
406
+ </mcms-card-title>
407
+ </mcms-card-header>
408
+ </mcms-card>
409
+
410
+ <mcms-card>
411
+ <mcms-card-header>
412
+ <div class="flex items-center justify-between">
413
+ <mcms-card-description>Active Requests</mcms-card-description>
414
+ <ng-icon name="heroSignal" class="text-muted-foreground" size="20" aria-hidden="true" />
415
+ </div>
416
+ <mcms-card-title>
417
+ <span class="text-3xl font-bold">{{ s.activeRequests }}</span>
418
+ </mcms-card-title>
419
+ </mcms-card-header>
420
+ </mcms-card>
421
+
422
+ <mcms-card>
423
+ <mcms-card-header>
424
+ <div class="flex items-center justify-between">
425
+ <mcms-card-description>Memory Usage</mcms-card-description>
426
+ <ng-icon name="heroCpuChip" class="text-muted-foreground" size="20" aria-hidden="true" />
427
+ </div>
428
+ <mcms-card-title>
429
+ <span class="text-3xl font-bold">{{ s.memoryUsageMb }} MB</span>
430
+ </mcms-card-title>
431
+ </mcms-card-header>
432
+ </mcms-card>
433
+ </div>
434
+ } @else if (otel.error(); as err) {
435
+ <div role="alert">
436
+ <mcms-card>
437
+ <mcms-card-header>
438
+ <mcms-card-title>Error loading observability data</mcms-card-title>
439
+ <mcms-card-description>{{ err }}</mcms-card-description>
440
+ </mcms-card-header>
441
+ <mcms-card-content>
442
+ <button
443
+ class="text-sm text-primary hover:underline cursor-pointer
444
+ focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
445
+ (click)="refresh()"
446
+ >
447
+ Try again
448
+ </button>
449
+ </mcms-card-content>
450
+ </mcms-card>
451
+ </div>
452
+ }
453
+ </section>
454
+
455
+ <!-- Request Metrics -->
456
+ @if (otel.summary(); as s) {
457
+ <section class="mb-10" aria-labelledby="request-metrics-heading">
458
+ <h2 id="request-metrics-heading" class="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">
459
+ Request Metrics
460
+ </h2>
461
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
462
+ <mcms-card>
463
+ <mcms-card-header>
464
+ <div class="flex items-center justify-between">
465
+ <mcms-card-description>Total Requests</mcms-card-description>
466
+ <ng-icon name="heroChartBarSquare" class="text-muted-foreground" size="20" aria-hidden="true" />
467
+ </div>
468
+ <mcms-card-title>
469
+ <span class="text-3xl font-bold">{{ s.requestMetrics.totalRequests }}</span>
470
+ </mcms-card-title>
471
+ </mcms-card-header>
472
+ </mcms-card>
473
+
474
+ <mcms-card>
475
+ <mcms-card-header>
476
+ <mcms-card-description>Avg Duration</mcms-card-description>
477
+ <mcms-card-title>
478
+ <span class="text-3xl font-bold">{{ s.requestMetrics.avgDurationMs }}ms</span>
479
+ </mcms-card-title>
480
+ </mcms-card-header>
481
+ </mcms-card>
482
+
483
+ <mcms-card>
484
+ <mcms-card-header>
485
+ <mcms-card-description>Errors</mcms-card-description>
486
+ <mcms-card-title>
487
+ <span class="text-3xl font-bold">{{ s.requestMetrics.errorCount }}</span>
488
+ </mcms-card-title>
489
+ </mcms-card-header>
490
+ </mcms-card>
491
+
492
+ <mcms-card>
493
+ <mcms-card-header>
494
+ <mcms-card-description>Error Rate</mcms-card-description>
495
+ <mcms-card-title>
496
+ <span class="text-3xl font-bold">{{ errorRate(s) }}%</span>
497
+ </mcms-card-title>
498
+ </mcms-card-header>
499
+ </mcms-card>
500
+ </div>
501
+
502
+ <!-- By Method + By Status Code -->
503
+ @if (methodEntries(s).length > 0 || statusEntries(s).length > 0) {
504
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
505
+ @if (methodEntries(s).length > 0) {
506
+ <mcms-card>
507
+ <mcms-card-header>
508
+ <mcms-card-title>By Method</mcms-card-title>
509
+ </mcms-card-header>
510
+ <mcms-card-content>
511
+ <div class="space-y-2">
512
+ @for (entry of methodEntries(s); track entry.name) {
513
+ <div class="flex items-center justify-between">
514
+ <span class="text-sm font-mono text-foreground">{{ entry.name }}</span>
515
+ <mcms-badge variant="secondary">{{ entry.count }}</mcms-badge>
516
+ </div>
517
+ }
518
+ </div>
519
+ </mcms-card-content>
520
+ </mcms-card>
521
+ }
522
+
523
+ @if (statusEntries(s).length > 0) {
524
+ <mcms-card>
525
+ <mcms-card-header>
526
+ <mcms-card-title>By Status Code</mcms-card-title>
527
+ </mcms-card-header>
528
+ <mcms-card-content>
529
+ <div class="space-y-2">
530
+ @for (entry of statusEntries(s); track entry.name) {
531
+ <div class="flex items-center justify-between">
532
+ <span class="text-sm font-mono text-foreground">{{ entry.name }}</span>
533
+ <mcms-badge [variant]="statusVariant(entry.name)">{{ entry.count }}</mcms-badge>
534
+ </div>
535
+ }
536
+ </div>
537
+ </mcms-card-content>
538
+ </mcms-card>
539
+ }
540
+ </div>
541
+ }
542
+ </section>
543
+
544
+ <!-- Collection Operations -->
545
+ @if (s.collectionMetrics.length > 0) {
546
+ <section class="mb-10" aria-labelledby="collection-ops-heading">
547
+ <h2 id="collection-ops-heading" class="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">
548
+ Collection Operations
549
+ </h2>
550
+ <div class="border border-border rounded-lg overflow-hidden">
551
+ <div class="overflow-x-auto">
552
+ <table class="w-full text-sm" aria-labelledby="collection-ops-heading">
553
+ <thead>
554
+ <tr class="border-b border-border bg-muted/50">
555
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
556
+ Collection
557
+ </th>
558
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
559
+ Creates
560
+ </th>
561
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
562
+ Updates
563
+ </th>
564
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
565
+ Deletes
566
+ </th>
567
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
568
+ Avg Duration
569
+ </th>
570
+ </tr>
571
+ </thead>
572
+ <tbody>
573
+ @for (col of s.collectionMetrics; track col.collection) {
574
+ <tr class="border-b border-border last:border-0 hover:bg-muted/30 transition-colors">
575
+ <td class="px-4 py-3 font-medium">
576
+ <mcms-badge variant="outline">{{ col.collection }}</mcms-badge>
577
+ </td>
578
+ <td class="px-4 py-3">{{ col.creates }}</td>
579
+ <td class="px-4 py-3">{{ col.updates }}</td>
580
+ <td class="px-4 py-3">{{ col.deletes }}</td>
581
+ <td class="px-4 py-3 text-muted-foreground">{{ col.avgDurationMs }}ms</td>
582
+ </tr>
583
+ }
584
+ </tbody>
585
+ </table>
586
+ </div>
587
+ </div>
588
+ </section>
589
+ }
590
+
591
+ <!-- Recent Traces -->
592
+ @if (s.recentSpans.length > 0) {
593
+ <section class="mb-10" aria-labelledby="recent-traces-heading">
594
+ <h2 id="recent-traces-heading" class="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">
595
+ Recent Traces
596
+ </h2>
597
+ <div class="border border-border rounded-lg overflow-hidden">
598
+ <div class="overflow-x-auto">
599
+ <table class="w-full text-sm" aria-labelledby="recent-traces-heading">
600
+ <thead>
601
+ <tr class="border-b border-border bg-muted/50">
602
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
603
+ Time
604
+ </th>
605
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
606
+ Trace ID
607
+ </th>
608
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
609
+ Operation
610
+ </th>
611
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
612
+ Duration
613
+ </th>
614
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
615
+ Status
616
+ </th>
617
+ </tr>
618
+ </thead>
619
+ <tbody>
620
+ @for (span of s.recentSpans; track span.spanId || $index) {
621
+ <tr class="border-b border-border last:border-0 hover:bg-muted/30 transition-colors">
622
+ <td class="px-4 py-3 text-muted-foreground whitespace-nowrap">
623
+ {{ formatTime(span.timestamp) }}
624
+ </td>
625
+ <td class="px-4 py-3 font-mono text-xs max-w-32 truncate">
626
+ <span [attr.aria-label]="'Trace ID: ' + span.traceId" [title]="span.traceId">
627
+ {{ truncateId(span.traceId) }}
628
+ </span>
629
+ </td>
630
+ <td class="px-4 py-3 font-medium">{{ span.name }}</td>
631
+ <td class="px-4 py-3 text-muted-foreground">{{ span.durationMs }}ms</td>
632
+ <td class="px-4 py-3">
633
+ <mcms-badge [variant]="spanStatusVariant(span)">{{ span.status }}</mcms-badge>
634
+ </td>
635
+ </tr>
636
+ }
637
+ </tbody>
638
+ </table>
639
+ </div>
640
+ </div>
641
+ </section>
642
+ }
643
+ }
644
+
645
+ <!-- Metrics History -->
646
+ <section class="mb-10" aria-labelledby="metrics-history-heading">
647
+ <div class="flex items-center justify-between mb-4">
648
+ <h2 id="metrics-history-heading" class="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
649
+ Metrics History
650
+ </h2>
651
+ <div class="flex items-center gap-2">
652
+ @for (range of historyRanges; track range.key) {
653
+ <button
654
+ class="px-3 py-1.5 text-xs font-medium rounded-md transition-colors cursor-pointer
655
+ focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
656
+ [class]="selectedRange() === range.key
657
+ ? 'bg-primary text-primary-foreground'
658
+ : 'border border-border bg-background text-foreground hover:bg-muted'"
659
+ (click)="selectRange(range.key)"
660
+ >
661
+ {{ range.label }}
662
+ </button>
663
+ }
664
+ </div>
665
+ </div>
666
+
667
+ <div class="flex items-center gap-3 mb-4">
668
+ <button
669
+ class="inline-flex items-center gap-2 px-3 py-1.5 text-xs font-medium rounded-md
670
+ border border-border bg-background text-foreground hover:bg-muted
671
+ transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed
672
+ focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
673
+ (click)="exportCsv()"
674
+ [disabled]="otel.exporting()"
675
+ aria-label="Export metrics history as CSV"
676
+ >
677
+ <ng-icon name="heroArrowDownTray" size="14" aria-hidden="true" />
678
+ {{ otel.exporting() ? 'Exporting...' : 'Export CSV' }}
679
+ </button>
680
+ <button
681
+ class="inline-flex items-center gap-2 px-3 py-1.5 text-xs font-medium rounded-md
682
+ transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed
683
+ focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
684
+ [class]="confirmingPurge()
685
+ ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
686
+ : 'border border-destructive/50 text-destructive hover:bg-destructive/10'"
687
+ (click)="confirmPurge()"
688
+ [disabled]="otel.purging()"
689
+ aria-label="Clear all metrics history"
690
+ >
691
+ <ng-icon name="heroTrash" size="14" aria-hidden="true" />
692
+ {{ otel.purging() ? 'Clearing...' : confirmingPurge() ? 'Confirm Clear' : 'Clear History' }}
693
+ </button>
694
+ @if (otel.historyTotal() > 0) {
695
+ <span class="text-xs text-muted-foreground">
696
+ {{ otel.historyTotal() }} snapshots stored
697
+ </span>
698
+ }
699
+ </div>
700
+
701
+ @if (otel.historyLoading()) {
702
+ <div class="border border-border rounded-lg p-8" aria-busy="true">
703
+ <div class="flex items-center justify-center gap-3 text-muted-foreground">
704
+ <mcms-skeleton class="h-4 w-48" />
705
+ </div>
706
+ </div>
707
+ } @else if (otel.history().length > 0) {
708
+ <div class="border border-border rounded-lg overflow-hidden">
709
+ <div class="overflow-x-auto">
710
+ <table class="w-full text-sm" aria-labelledby="metrics-history-heading">
711
+ <thead>
712
+ <tr class="border-b border-border bg-muted/50">
713
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
714
+ Timestamp
715
+ </th>
716
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
717
+ Requests
718
+ </th>
719
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
720
+ Errors
721
+ </th>
722
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
723
+ Avg Duration
724
+ </th>
725
+ <th scope="col" class="px-4 py-3 text-left font-medium text-muted-foreground">
726
+ Memory
727
+ </th>
728
+ </tr>
729
+ </thead>
730
+ <tbody>
731
+ @for (snap of otel.history(); track snap.id || $index) {
732
+ <tr class="border-b border-border last:border-0 hover:bg-muted/30 transition-colors">
733
+ <td class="px-4 py-3 text-muted-foreground whitespace-nowrap">
734
+ {{ formatSnapshotTime(snap.createdAt) }}
735
+ </td>
736
+ <td class="px-4 py-3 font-medium">{{ snap.totalRequests }}</td>
737
+ <td class="px-4 py-3">
738
+ <mcms-badge [variant]="snap.errorCount > 0 ? 'destructive' : 'secondary'">
739
+ {{ snap.errorCount }}
740
+ </mcms-badge>
741
+ </td>
742
+ <td class="px-4 py-3 text-muted-foreground">{{ snap.avgDurationMs }}ms</td>
743
+ <td class="px-4 py-3 text-muted-foreground">{{ snap.memoryUsageMb }} MB</td>
744
+ </tr>
745
+ }
746
+ </tbody>
747
+ </table>
748
+ </div>
749
+ </div>
750
+ } @else {
751
+ <div class="border border-border rounded-lg p-8 text-center text-muted-foreground text-sm">
752
+ No history snapshots found for this time range.
753
+ </div>
754
+ }
755
+ </section>
756
+ `
757
+ })
758
+ ], OtelDashboardPage);
759
+ }
760
+ });
761
+
762
+ // libs/plugins/otel/src/lib/otel-admin-routes.ts
763
+ var otel_admin_routes_exports = {};
764
+ __export(otel_admin_routes_exports, {
765
+ otelAdminRoutes: () => otelAdminRoutes
766
+ });
767
+ module.exports = __toCommonJS(otel_admin_routes_exports);
768
+ var otelAdminRoutes = [
769
+ {
770
+ path: "observability",
771
+ label: "Observability",
772
+ icon: "heroSignal",
773
+ loadComponent: () => Promise.resolve().then(() => (init_otel_dashboard_page(), otel_dashboard_page_exports)).then((m) => m.OtelDashboardPage),
774
+ group: "Plugins"
775
+ }
776
+ ];
777
+ // Annotate the CommonJS export names for ESM import in node:
778
+ 0 && (module.exports = {
779
+ otelAdminRoutes
780
+ });