Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions src/lib/components/ProjectMetricsByDeployment.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<script lang="ts">
import { tick, untrack } from 'svelte'
import { page } from '$app/stores'
import { replaceState } from '$app/navigation'
import api from '$lib/api'
import Chart from '$lib/components/Chart.svelte'
import Select from '$lib/components/Select.svelte'
import type { MetricSeries } from '$lib/charts/util'

/**
* Daily per-deployment charts from project.metricsByDeployment. The samples
* already live on each deployment (30-day retention), so this view does not
* need a new collector or usage table. CPU and memory are a day's average
* summed across pods; egress and requests are daily totals.
*/

interface Props {
project: string
}

const { project }: Props = $props()

const rangeOptions = [
{ value: '7d', label: '7 Days' },
{ value: '30d', label: '30 Days' }
]

const requested = $page.url.searchParams.get('range')
const filter = $state({
range: requested === '7d' || requested === '30d' ? requested : '30d'
})

let cpu = $state<MetricSeries[]>([])
let memory = $state<MetricSeries[]>([])
let egress = $state<MetricSeries[]>([])
let requests = $state<MetricSeries[]>([])
let storage = $state<MetricSeries[]>([])

function seriesOf (lines: Api.UsageMetricsLine[] | null | undefined): MetricSeries[] {
return (lines ?? []).map((line) => ({
prefix: line.name,
lines: [{ name: line.name, points: line.points ?? [] }]
}))
}

async function fetchMetrics (clear = false) {
const range = untrack(() => filter.range)
const resp: Api.Response<Api.ProjectMetricsByDeploymentResult> = await api.invoke(
'project.metricsByDeployment',
{ project, timeRange: range },
fetch
)
if (!resp.ok) {
return
}

if (clear) {
cpu = []
memory = []
egress = []
requests = []
storage = []
await tick()
}

cpu = seriesOf(resp.result.cpuUsage)
memory = seriesOf(resp.result.memory)
egress = seriesOf(resp.result.egress)
requests = seriesOf(resp.result.requests)
storage = seriesOf(resp.result.staticStorage)
}

function selectRange () {
const u = new URL($page.url)
u.searchParams.set('range', filter.range)
replaceState(u, {})
fetchMetrics(true)
}

$effect(() => {
fetchMetrics(true)
})
</script>

<p class="text-sm text-content/60 mb-4">
CPU and memory are each day's average, summed across a deployment's pods. Egress and requests are daily totals.
Samples are kept for 30 days. Cache egress, replicas, and disk are not recorded per deployment.
</p>

<div class="w-44 max-w-full">
<Select
bind:value={filter.range}
options={rangeOptions}
onchange={selectRange} />
</div>

<div class="grid gap-4 mt-4 lg:grid-cols-2">
<Chart title="CPU (vCPU)" unit="count" series={cpu} range={filter.range} />
<Chart title="Memory (bytes)" unit="bytes" series={memory} range={filter.range} />
<Chart title="Egress (bytes)" unit="bytes" series={egress} range={filter.range} />
<Chart title="Requests" unit="count" series={requests} range={filter.range} />
<Chart title="Static Storage (bytes)" unit="bytes" series={storage} range={filter.range} />
</div>
21 changes: 21 additions & 0 deletions src/lib/server/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1796,6 +1796,27 @@ const handlers: Record<string, (args: any) => object> = {
replica: dailyMetricLine('replica', 3),
staticStorage: dailyMetricLine('static_storage', 1610612736)
}),
'project.metricsByDeployment': () => ok({
cpuUsage: [
dailyMetricLine('web', 1.1)[0],
dailyMetricLine('worker', 0.35)[0]
],
memory: [
dailyMetricLine('web', 536870912)[0],
dailyMetricLine('worker', 268435456)[0]
],
egress: [
dailyMetricLine('web', 800000000)[0],
dailyMetricLine('worker', 120000000)[0]
],
requests: [
dailyMetricLine('web', 42000)[0],
dailyMetricLine('site', 18000)[0]
],
staticStorage: [
dailyMetricLine('site', 1610612736)[0]
]
}),

'billing.list': () => list(billingAccounts),
'billing.get': (args) => ok(billingAccounts.find((b) => b.id === args?.id) ?? { ...billingAccounts[0], id: args?.id ?? 'ba_mock_1' }),
Expand Down
40 changes: 38 additions & 2 deletions src/routes/(auth)/(project)/metrics/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,16 +1,52 @@
<script lang="ts">
import { page } from '$app/stores'
import { goto } from '$app/navigation'
import type { PageData } from './$types'
import ProjectMetrics from '$lib/components/ProjectMetrics.svelte'
import ProjectMetricsByDeployment from '$lib/components/ProjectMetricsByDeployment.svelte'

const { data }: { data: PageData } = $props()
const project = $derived(data.project)
const tab = $derived($page.url.searchParams.get('tab') === 'deployment' ? 'deployment' : 'project')

function tabHref (value: string): string {
const u = new URL($page.url)
if (value === 'deployment') u.searchParams.set('tab', 'deployment')
else u.searchParams.delete('tab')
return `${u.pathname}${u.search}`
}
</script>

<div class="page-head">
<div>
<h4><strong>Metrics</strong></h4>
<p class="page-sub">Daily project usage — CPU, memory, egress, replicas, and static storage</p>
<p class="page-sub">
{#if tab === 'deployment'}
Daily usage grouped by deployment
{:else}
Daily project usage — CPU, memory, egress, replicas, and static storage
{/if}
</p>
</div>
</div>

<ProjectMetrics {project} />
<div class="select lg:hidden mb-4">
<select
aria-label="Metrics view"
value={tab}
onchange={(e) => goto(tabHref((e.currentTarget as HTMLSelectElement).value))}
>
<option value="project">Project</option>
<option value="deployment">By deployment</option>
</select>
</div>
<div class="tabs is-variant-underline w-full max-lg:hidden mb-4">
<a class="tab-button" class:is-active={tab === 'project'} href={tabHref('project')}>Project</a>
<a class="tab-button" class:is-active={tab === 'deployment'} href={tabHref('deployment')}>By deployment</a>
</div>

{#if tab === 'deployment'}
<ProjectMetricsByDeployment {project} />
{:else}
<ProjectMetrics {project} />
{/if}
10 changes: 10 additions & 0 deletions src/types/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,16 @@ declare namespace Api {
staticStorage: UsageMetricsLine[]
}

// Result of project.metricsByDeployment. One line per deployment. History
// is 30 days (7d or 30d). Disk, replicas, and cache egress are not included.
export type ProjectMetricsByDeploymentResult = {
cpuUsage: UsageMetricsLine[]
memory: UsageMetricsLine[]
egress: UsageMetricsLine[]
requests: UsageMetricsLine[]
staticStorage: UsageMetricsLine[]
}

// Result of deployment.metrics. Every series is optional — the backend only
// returns the metrics relevant to the deployment type (e.g. requests for
// WebService/Static, storage for Static).
Expand Down
Loading