@momentumcms/plugins-otel 0.5.4 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.cjs +1074 -26
- package/index.js +1082 -26
- package/lib/otel-admin-routes.cjs +780 -0
- package/lib/otel-admin-routes.js +788 -0
- package/package.json +61 -36
- package/src/index.d.ts +1 -1
- package/src/lib/api/otel-api-guards.d.ts +27 -0
- package/src/lib/api/otel-query-handler.d.ts +20 -0
- package/src/lib/exporters/prometheus-handler.d.ts +20 -0
- package/src/lib/metrics/collection-metrics.d.ts +21 -0
- package/src/lib/metrics/metrics-snapshot-service.d.ts +31 -0
- package/src/lib/metrics/metrics-store.d.ts +31 -0
- package/src/lib/metrics/otel-helpers.d.ts +42 -0
- package/src/lib/metrics/otel-snapshot-collection.d.ts +7 -0
- package/src/lib/metrics/request-metrics.d.ts +19 -0
- package/src/lib/otel-admin-routes.d.ts +3 -0
- package/src/lib/otel-plugin.d.ts +11 -7
- package/src/lib/otel-plugin.types.d.ts +79 -1
- package/CHANGELOG.md +0 -110
- package/LICENSE +0 -21
|
@@ -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
|
+
});
|