Add autoscaling outcomes to Metrics (Refs #140)

This commit is contained in:
2026-10-10 17:51:31 -04:00
parent 0bfda267ec
commit 528ed7f37a
15 changed files with 612 additions and 6 deletions
@@ -0,0 +1,70 @@
<script lang="ts">
import type { AutoscalingMetrics } from '$lib/metrics.js';
import { metricNumber } from '$ui/metrics.js';
import { countLabel } from '$ui/plural.js';
import Card from '$ui/components/primitives/Card.svelte';
import Stat from '$ui/components/primitives/Stat.svelte';
let { metrics }: { metrics: AutoscalingMetrics } = $props();
const outcomes = $derived(metrics.added + metrics.removed + metrics.failed + metrics.cancelled);
const seconds = (value: number | null) => (value === null ? '—' : `${metricNumber(value)} s`);
</script>
<Card as="section" ariaLabelledby="metrics-autoscaling-title">
<header>
<h2 id="metrics-autoscaling-title">Autoscaling</h2>
<a href="/autoscaling">Open policies</a>
</header>
<div class="stats">
<Stat label="Lobbies added" value={metrics.added} tween={false} />
<Stat label="Lobbies removed" value={metrics.removed} tween={false} />
<Stat label="Failed operations" value={metrics.failed} tween={false} />
<Stat label="Cancelled operations" value={metrics.cancelled} tween={false} />
<Stat label="Request to ready · average" value={seconds(metrics.readinessMeanSeconds)} />
<Stat label="Request to ready · maximum" value={seconds(metrics.readinessMaxSeconds)} />
</div>
<p>
{#if outcomes === 0}
No completed, failed or cancelled autoscaling operations recorded in this period and
scope.
{:else}
Autoscaling operations finished in this period and scope. Added and removed counts
include successful operations only.
{/if}
{#if metrics.readinessSamples > 0}
Readiness uses {countLabel(metrics.readinessSamples, 'completed create')} with recorded timing,
including queue time.
{:else}
Request-to-ready time is unavailable without completed creates with recorded timing.
{/if}
</p>
</Card>
<style>
header {
display: flex;
align-items: baseline;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-3);
margin-bottom: var(--space-5);
}
h2 {
margin: 0;
font-size: var(--text-lg);
}
a {
font-size: var(--text-sm);
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: var(--space-5);
}
p {
margin: var(--space-4) 0 0;
color: var(--text-tertiary);
font-size: var(--text-xs);
line-height: 1.6;
}
</style>
+10 -1
View File
@@ -24,6 +24,14 @@ const matches = (value: unknown) =>
) &&
(value.averageGroupSize === null || nonnegative(value.averageGroupSize)) &&
(value.soloShare === null || (nonnegative(value.soloShare) && value.soloShare <= 100));
const autoscaling = (value: unknown) =>
record(value) &&
['added', 'removed', 'failed', 'cancelled', 'readinessSamples'].every(
(key) => nonnegative(value[key]) && Number.isInteger(value[key])
) &&
['readinessMeanSeconds', 'readinessMaxSeconds'].every(
(key) => value[key] === null || nonnegative(value[key])
);
const policy = (value: unknown) =>
value === null ||
(record(value) &&
@@ -40,7 +48,8 @@ function validReport(value: unknown): value is MetricsData {
!numeric(value.since) ||
!totals(value.summary) ||
!running(value.running) ||
!matches(value.matches)
!matches(value.matches) ||
!autoscaling(value.autoscaling)
)
return false;
if (