Skip to content

Commit b0b2534

Browse files
committed
Metrics Archive Run page
1 parent 4887291 commit b0b2534

5 files changed

Lines changed: 308 additions & 17 deletions

File tree

apps/api-manager/src/lib/config/navigation.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
MessageSquare,
4646
Mail,
4747
Stethoscope,
48+
Play,
4849
} from "@lucide/svelte";
4950
import { env } from "$env/dynamic/public";
5051

@@ -272,6 +273,11 @@ function buildMetricsItems(): NavigationItem[] {
272273
label: "Diagnostics",
273274
iconComponent: Stethoscope,
274275
},
276+
{
277+
href: "/metrics-archive-run",
278+
label: "Trigger Archive Run",
279+
iconComponent: Play,
280+
},
275281
];
276282

277283
return items;
@@ -755,7 +761,7 @@ export const navSections: NavigationSection[] = [
755761
{ id: "system", label: "System", iconComponent: Server, items: systemItems, basePaths: ["/system"] },
756762
{ id: "signals", label: "Signals", iconComponent: Radio, items: signalsItems, basePaths: ["/system/signal-publish", "/system/signal-channels", "/system/signal-channels-stats"] },
757763
{ id: "integration", label: "Integration", iconComponent: Plug, items: integrationItems, basePaths: ["/integration"] },
758-
{ id: "metrics", label: "Metrics", iconComponent: BarChart3, items: metricsItems, basePaths: ["/metrics", "/aggregate-metrics", "/aggregate-metrics-live", "/aggregate-metrics-trends", "/connector-metrics", "/connector-traces", "/connector-counts", "/metrics-diagnostics"] },
764+
{ id: "metrics", label: "Metrics", iconComponent: BarChart3, items: metricsItems, basePaths: ["/metrics", "/aggregate-metrics", "/aggregate-metrics-live", "/aggregate-metrics-trends", "/connector-metrics", "/connector-traces", "/connector-counts", "/metrics-diagnostics", "/metrics-archive-run"] },
759765
{ id: "abac", label: "ABAC", iconComponent: Lock, items: abacItems, basePaths: ["/abac"] },
760766
{ id: "products", label: "API Products", iconComponent: Package, items: productsItems, basePaths: ["/products"] },
761767
{ id: "financial-products", label: "Financial Products", iconComponent: Banknote, items: financialProductsItems, basePaths: ["/products/financial", "/products/collections"] },

apps/api-manager/src/lib/utils/roleChecker.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,9 @@ export const SITE_MAP: Record<string, PageRoleConfig> = {
220220
"/metrics-diagnostics": {
221221
required: [{ role: "CanGetMetricsDiagnostics" }],
222222
},
223+
"/metrics-archive-run": {
224+
required: [{ role: "CanCreateMetricsArchiveRun" }],
225+
},
223226

224227
// ── ABAC ──────────────────────────────────────────────
225228
"/abac/rules": {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { PageServerLoad } from "./$types";
2+
import { error } from "@sveltejs/kit";
3+
import { createLogger } from "@obp/shared/utils";
4+
import { SessionOAuthHelper } from "$lib/oauth/sessionHelper";
5+
6+
const logger = createLogger("MetricsArchiveRunPageServer");
7+
8+
export const load: PageServerLoad = async ({ locals }) => {
9+
logger.info("=== Metrics Archive Run Page Load Started ===");
10+
11+
const session = locals.session;
12+
13+
if (!session?.data?.user) {
14+
logger.error("No user in session");
15+
throw error(401, "Unauthorized");
16+
}
17+
18+
const sessionOAuth = SessionOAuthHelper.getSessionOAuth(session);
19+
const accessToken = sessionOAuth?.accessToken;
20+
21+
if (!accessToken) {
22+
logger.error("No access token available");
23+
throw error(401, "No API access token available");
24+
}
25+
26+
const userEntitlements =
27+
(session.data.user as any)?.entitlements?.list || [];
28+
29+
const hasRole = userEntitlements.some(
30+
(ent: any) => ent.role_name === "CanCreateMetricsArchiveRun",
31+
);
32+
33+
return {
34+
hasRole,
35+
userEntitlements,
36+
};
37+
};
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
<script lang="ts">
2+
import type { PageData } from "./$types";
3+
import MissingRoleAlert from "$lib/components/MissingRoleAlert.svelte";
4+
import {
5+
Play,
6+
CheckCircle2,
7+
AlertTriangle,
8+
XCircle,
9+
Loader2,
10+
} from "@lucide/svelte";
11+
12+
let { data }: { data: PageData } = $props();
13+
14+
const RUN_ENDPOINT = "/proxy/obp/v7.0.0/management/system/diagnostics/metrics/run";
15+
16+
let running = $state(false);
17+
let result = $state<any>(null);
18+
let errorMessage = $state<string | null>(null);
19+
20+
async function triggerRun() {
21+
running = true;
22+
result = null;
23+
errorMessage = null;
24+
try {
25+
const response = await fetch(RUN_ENDPOINT, { method: "POST" });
26+
const body = await response.json();
27+
if (!response.ok) {
28+
errorMessage = body.message ?? `HTTP ${response.status}`;
29+
return;
30+
}
31+
result = body;
32+
} catch (err) {
33+
errorMessage = err instanceof Error ? err.message : "Failed to trigger archive run";
34+
} finally {
35+
running = false;
36+
}
37+
}
38+
39+
function formatDate(value: string | null | undefined): string {
40+
if (!value) return "";
41+
const d = new Date(value);
42+
if (isNaN(d.getTime())) return String(value);
43+
return d.toLocaleString();
44+
}
45+
46+
function formatNumber(value: number | null | undefined): string {
47+
if (value === null || value === undefined) return "";
48+
return value.toLocaleString();
49+
}
50+
51+
// status is "completed" or "skipped_already_in_progress"; within "completed",
52+
// run.success tells you whether the run itself succeeded.
53+
const isCompleted = $derived(result?.status === "completed");
54+
const isSkipped = $derived(result?.status === "skipped_already_in_progress");
55+
const runSucceeded = $derived(result?.run?.success === true);
56+
57+
// run.* prop -> displayed value, so every field is traceable to the response.
58+
const runFields = $derived(
59+
result?.run
60+
? [
61+
{ prop: "run.run_id", value: result.run.run_id },
62+
{ prop: "run.api_instance_id", value: result.run.api_instance_id },
63+
{ prop: "run.started_at", value: formatDate(result.run.started_at) },
64+
{ prop: "run.ended_at", value: formatDate(result.run.ended_at) },
65+
{ prop: "run.duration_ms", value: formatNumber(result.run.duration_ms) },
66+
{ prop: "run.rows_moved_to_archive", value: formatNumber(result.run.rows_moved_to_archive) },
67+
{ prop: "run.rows_deleted_from_archive", value: formatNumber(result.run.rows_deleted_from_archive) },
68+
{ prop: "run.success", value: String(result.run.success) },
69+
{ prop: "run.remark", value: result.run.remark === "" ? "" : result.run.remark },
70+
]
71+
: [],
72+
);
73+
</script>
74+
75+
<svelte:head>
76+
<title>Trigger Metrics Archive Run - API Manager</title>
77+
</svelte:head>
78+
79+
<div class="container mx-auto px-4 py-8" data-testid="metrics-archive-run-page">
80+
<!-- Header -->
81+
<div class="mb-6">
82+
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
83+
Trigger Metrics Archive Run
84+
</h1>
85+
<p class="mt-1 text-gray-600 dark:text-gray-400">
86+
Run the metrics-archiving job once, on demand. This invokes the same code
87+
path as the scheduled <code class="font-mono">MetricsArchiveScheduler</code>:
88+
it moves <code class="font-mono">metric</code> rows older than the effective
89+
<code class="font-mono">retain_metrics_days</code> into
90+
<code class="font-mono">metricarchive</code> (up to
91+
<code class="font-mono">retain_metrics_move_limit</code> rows), deletes
92+
<code class="font-mono">metricarchive</code> rows past the effective
93+
<code class="font-mono">retain_archive_metrics_days</code>, respects the
94+
concurrency lock, and records the outcome in
95+
<code class="font-mono">metricsarchiverun</code>. It runs synchronously, so
96+
a large backlog may take a while.
97+
</p>
98+
</div>
99+
100+
{#if !data.hasRole}
101+
<MissingRoleAlert
102+
roles={["CanCreateMetricsArchiveRun"]}
103+
message="You need this role to trigger a metrics archive run"
104+
/>
105+
{:else}
106+
<!-- Trigger -->
107+
<div class="mb-6">
108+
<button
109+
type="button"
110+
onclick={triggerRun}
111+
disabled={running}
112+
data-testid="trigger-run"
113+
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
114+
>
115+
{#if running}
116+
<Loader2 class="h-4 w-4 animate-spin" />
117+
Running…
118+
{:else}
119+
<Play class="h-4 w-4" />
120+
Run Archive Now
121+
{/if}
122+
</button>
123+
</div>
124+
125+
<!-- Error -->
126+
{#if errorMessage}
127+
<div
128+
class="mb-6 rounded-lg border border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20"
129+
data-testid="run-error"
130+
>
131+
<div class="flex items-center gap-2">
132+
<XCircle class="h-5 w-5 text-red-600 dark:text-red-400" />
133+
<p class="text-sm font-medium text-red-800 dark:text-red-200">{errorMessage}</p>
134+
</div>
135+
</div>
136+
{/if}
137+
138+
<!-- Result -->
139+
{#if result}
140+
<!-- Status banner -->
141+
<div
142+
class="mb-6 rounded-lg border p-4 {isSkipped
143+
? 'border-yellow-200 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-900/20'
144+
: runSucceeded
145+
? 'border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-900/20'
146+
: 'border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-900/20'}"
147+
data-testid="run-status"
148+
data-status={result.status}
149+
>
150+
<div class="flex items-start gap-3">
151+
{#if isSkipped}
152+
<AlertTriangle class="mt-0.5 h-6 w-6 text-yellow-600 dark:text-yellow-400" />
153+
{:else if runSucceeded}
154+
<CheckCircle2 class="mt-0.5 h-6 w-6 text-green-600 dark:text-green-400" />
155+
{:else}
156+
<XCircle class="mt-0.5 h-6 w-6 text-red-600 dark:text-red-400" />
157+
{/if}
158+
<div>
159+
<div class="flex items-center gap-2">
160+
<span class="text-base font-semibold text-gray-900 dark:text-gray-100">
161+
{#if isSkipped}
162+
Skipped — already in progress
163+
{:else if runSucceeded}
164+
Run completed
165+
{:else}
166+
Run completed with errors
167+
{/if}
168+
</span>
169+
<code class="rounded bg-gray-100 px-1.5 py-0.5 text-xs font-mono text-gray-700 dark:bg-gray-900 dark:text-gray-300">status</code>
170+
</div>
171+
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">{result.message}</p>
172+
<code class="text-xs font-mono text-gray-500 dark:text-gray-400">message</code>
173+
</div>
174+
</div>
175+
</div>
176+
177+
<!-- Run detail -->
178+
{#if isCompleted && runFields.length > 0}
179+
<div class="mb-6 rounded-lg border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
180+
<div class="border-b border-gray-200 p-6 dark:border-gray-700">
181+
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
182+
Recorded Run
183+
</h2>
184+
</div>
185+
<div class="grid grid-cols-1 gap-px overflow-hidden rounded-b-lg bg-gray-200 dark:bg-gray-700 sm:grid-cols-2 lg:grid-cols-3">
186+
{#each runFields as field (field.prop)}
187+
<div class="bg-white p-4 dark:bg-gray-800" data-testid="run-field" data-prop={field.prop}>
188+
<code class="text-sm font-mono text-gray-600 dark:text-gray-400">{field.prop}</code>
189+
<p class="mt-1 text-lg font-semibold break-words text-gray-900 dark:text-gray-100">
190+
{field.value}
191+
</p>
192+
</div>
193+
{/each}
194+
</div>
195+
</div>
196+
{/if}
197+
198+
<!-- Raw response -->
199+
<div class="rounded-lg border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
200+
<div class="border-b border-gray-200 p-6 dark:border-gray-700">
201+
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
202+
Raw Response
203+
</h2>
204+
</div>
205+
<div class="p-6">
206+
<div class="overflow-auto max-h-[600px]">
207+
<pre class="whitespace-pre-wrap break-words rounded-lg bg-gray-50 p-4 text-xs dark:bg-gray-900"><code
208+
class="text-gray-900 dark:text-gray-100">{JSON.stringify(result, null, 2)}</code></pre>
209+
</div>
210+
</div>
211+
</div>
212+
{/if}
213+
{/if}
214+
</div>

apps/api-manager/src/routes/(protected)/metrics-diagnostics/+page.svelte

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,34 @@
3838
return `${days.toLocaleString()} day${days === 1 ? "" : "s"}`;
3939
}
4040
41+
// Check names are descriptive slugs, not OBP props — drop the underscores so
42+
// they don't masquerade as prop names. Actual props are printed verbatim.
43+
function humanize(name: string): string {
44+
const s = name.replace(/_/g, " ");
45+
return s.charAt(0).toUpperCase() + s.slice(1);
46+
}
47+
48+
// The props each derived integrity check is computed from, so every statement
49+
// on the page is traceable to a real field in the API response. Keyed by the
50+
// backend check.name; "+7d grace" in the messages is a hardcoded constant.
51+
const checkProps: Record<string, string[]> = {
52+
write_metrics_enabled: ["config.write_metrics"],
53+
metrics_scheduler_enabled: ["config.enable_metrics_scheduler"],
54+
metric_oldest_within_retention: [
55+
"metric.oldest_record_age_days",
56+
"config.retain_metrics_days_effective",
57+
],
58+
archive_oldest_within_retention: [
59+
"metric_archive.oldest_record_age_days",
60+
"config.retain_archive_metrics_days_effective",
61+
],
62+
archive_recently_updated: [
63+
"metric.oldest_record_age_days",
64+
"metric_archive.newest_record_age_days",
65+
"config.retain_metrics_days_effective",
66+
],
67+
};
68+
4169
function checkBadgeColor(status: string): string {
4270
if (status === "OK")
4371
return "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400";
@@ -166,23 +194,18 @@
166194
</h2>
167195
</div>
168196
<div class="grid grid-cols-1 gap-px overflow-hidden rounded-b-lg bg-gray-200 dark:bg-gray-700 sm:grid-cols-2 lg:grid-cols-3">
169-
{#each [{ key: "write_metrics", label: "Write Metrics", value: config?.write_metrics }, { key: "enable_metrics_scheduler", label: "Scheduler Enabled", value: config?.enable_metrics_scheduler }, { key: "retain_metrics_move_limit", label: "Move Batch Limit", value: config?.retain_metrics_move_limit }, { key: "retain_metrics_days", label: "Retain Metrics (days)", value: config?.retain_metrics_days, effective: config?.retain_metrics_days_effective }, { key: "retain_archive_metrics_days", label: "Retain Archive (days)", value: config?.retain_archive_metrics_days, effective: config?.retain_archive_metrics_days_effective }] as item (item.key)}
170-
<div class="bg-white p-4 dark:bg-gray-800" data-testid="config-item" data-config-key={item.key}>
171-
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">
172-
{item.label}
173-
</p>
197+
{#each Object.entries(config) as [key, value] (key)}
198+
<div class="bg-white p-4 dark:bg-gray-800" data-testid="config-item" data-config-key={key}>
199+
<code class="text-sm font-mono text-gray-600 dark:text-gray-400">
200+
{key}
201+
</code>
174202
<p class="mt-1 text-lg font-semibold text-gray-900 dark:text-gray-100">
175-
{#if typeof item.value === "boolean"}
176-
{item.value ? "Yes" : "No"}
203+
{#if typeof value === "boolean"}
204+
{value ? "true" : "false"}
177205
{:else}
178-
{item.value ?? ""}
206+
{value}
179207
{/if}
180208
</p>
181-
{#if item.effective !== undefined && item.effective !== item.value}
182-
<p class="text-xs text-gray-500 dark:text-gray-400">
183-
effective: {item.effective}
184-
</p>
185-
{/if}
186209
</div>
187210
{/each}
188211
</div>
@@ -211,16 +234,24 @@
211234
</div>
212235
<div class="flex-1">
213236
<div class="flex items-center gap-2">
214-
<code class="text-sm font-mono font-semibold text-gray-900 dark:text-gray-100">
215-
{check.name}
216-
</code>
237+
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">
238+
{humanize(check.name)}
239+
</span>
217240
<span class="rounded-full px-2.5 py-0.5 text-xs font-medium {checkBadgeColor(check.status)}">
218241
{check.status}
219242
</span>
220243
</div>
221244
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
222245
{check.message}
223246
</p>
247+
{#if checkProps[check.name]}
248+
<div class="mt-2 flex flex-wrap items-center gap-1.5" data-testid="check-props">
249+
<span class="text-xs text-gray-500 dark:text-gray-400">props:</span>
250+
{#each checkProps[check.name] as prop (prop)}
251+
<code class="rounded bg-gray-100 px-1.5 py-0.5 text-xs font-mono text-gray-700 dark:bg-gray-900 dark:text-gray-300">{prop}</code>
252+
{/each}
253+
</div>
254+
{/if}
224255
</div>
225256
</li>
226257
{/each}

0 commit comments

Comments
 (0)