mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
Add system resources settings page (#328)
## Summary Adds server-visible resource reporting and a compact Resources settings tab for CPU, memory, and the filesystem that contains Fabro storage. ## Changes - Adds `GET /api/v1/system/resources` backed by `sysinfo`, including CPU sampling, cgroup-aware memory reporting, storage filesystem matching, and Fabro-managed disk byte totals. - Extends the OpenAPI contract and regenerates the Rust and TypeScript API clients. - Adds a deterministic demo-mode resources route. - Adds `/settings/resources` with 5 second polling and panels for overview, CPU, memory, disk, and notes. - Adds server integration/unit coverage and web route/render coverage. ## Screenshot  ## Verification - `cargo build -p fabro-api` - `cd lib/packages/fabro-api-client && bun run generate` - `cargo nextest run -p fabro-server --features test-support --test it api::system` - `cargo test -p fabro-server resource_sampler::tests` - `cd apps/fabro-web && bun test` - `cd apps/fabro-web && bun run typecheck` - `cd apps/fabro-web && bun run build` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` --- [](https://github.com/compound-engineering) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5eb874b55c
commit
40ed64c1c2
29 changed files with 1608 additions and 8 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -2304,6 +2304,7 @@ dependencies = [
|
|||
"serde_yaml",
|
||||
"sha2",
|
||||
"strum",
|
||||
"sysinfo",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
|
|
|
|||
|
|
@ -82,7 +82,10 @@ export function formatDurationSecs(secs: number): string {
|
|||
|
||||
export function formatDurationMs(ms: number): string {
|
||||
if (ms < 1000) return `${Math.round(ms)}ms`;
|
||||
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
|
||||
if (ms < 60_000) {
|
||||
const seconds = ms / 1000;
|
||||
return `${Number.isInteger(seconds) ? seconds : seconds.toFixed(1)}s`;
|
||||
}
|
||||
const minutes = Math.floor(ms / 60_000);
|
||||
const seconds = Math.round((ms % 60_000) / 1000);
|
||||
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import type {
|
|||
SandboxServiceListResponse,
|
||||
ServerSettings,
|
||||
SystemInfoResponse,
|
||||
SystemResourcesResponse,
|
||||
VncPreviewResponse,
|
||||
WorkflowDetailResponse,
|
||||
WorkflowSettings,
|
||||
|
|
@ -93,6 +94,14 @@ export function useSystemInfo() {
|
|||
);
|
||||
}
|
||||
|
||||
export function useSystemResources() {
|
||||
return useSWR<SystemResourcesResponse>(
|
||||
queryKeys.system.resources(),
|
||||
() => apiData(() => systemApi.getSystemResources()),
|
||||
{ refreshInterval: 5_000 },
|
||||
);
|
||||
}
|
||||
|
||||
export function useBoardsRuns(includeArchived: boolean = false) {
|
||||
return useSWR<BoardRunsEnvelope>(
|
||||
queryKeys.boards.runs(includeArchived),
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export const queryKeys = {
|
|||
},
|
||||
system: {
|
||||
info: () => ["system", "info"] as const,
|
||||
resources: () => ["system", "resources"] as const,
|
||||
attachUrl: () => "/api/v1/attach",
|
||||
},
|
||||
boards: {
|
||||
|
|
|
|||
|
|
@ -32,4 +32,10 @@ describe("browser router", () => {
|
|||
expect(paths).toContain("/setup");
|
||||
expect(paths).not.toContain("/setup/complete");
|
||||
});
|
||||
|
||||
test("exposes the resources settings page", () => {
|
||||
const paths = collectPaths(routes);
|
||||
|
||||
expect(paths).toContain("/settings/resources");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import * as Settings from "./routes/settings";
|
|||
import * as SettingsGeneral from "./routes/settings-general";
|
||||
import * as SettingsIntegrations from "./routes/settings-integrations";
|
||||
import * as SettingsModels from "./routes/settings-models";
|
||||
import * as SettingsResources from "./routes/settings-resources";
|
||||
import * as SettingsSecurity from "./routes/settings-security";
|
||||
import * as SettingsStorage from "./routes/settings-storage";
|
||||
import * as SettingsLiveEvents from "./routes/settings-live-events";
|
||||
|
|
@ -139,6 +140,7 @@ export const routes: RouteObject[] = [
|
|||
route("models", SettingsModels),
|
||||
route("security", SettingsSecurity),
|
||||
route("storage", SettingsStorage),
|
||||
route("resources", SettingsResources),
|
||||
route("live-events", SettingsLiveEvents),
|
||||
],
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ import { useState } from "react";
|
|||
import type {
|
||||
ServerListenSettings,
|
||||
ServerSettings,
|
||||
SystemInfoResponse,
|
||||
} from "@qltysh/fabro-api-client";
|
||||
import { useServerSettings } from "../lib/queries";
|
||||
import { formatDurationSecs } from "../lib/format";
|
||||
import { useServerSettings, useSystemInfo } from "../lib/queries";
|
||||
import { CollapsibleFile } from "../components/collapsible-file";
|
||||
import {
|
||||
Badge,
|
||||
|
|
@ -48,7 +50,10 @@ export default function SettingsGeneral() {
|
|||
<div className="space-y-6">
|
||||
<SettingsPageIntro description={DESCRIPTION} view={view} setView={setView} />
|
||||
{view === "settings" ? (
|
||||
<ServerPanel settings={settings} />
|
||||
<>
|
||||
<ServerPanel settings={settings} />
|
||||
<SystemPanel />
|
||||
</>
|
||||
) : (
|
||||
<CollapsibleFile
|
||||
file={{
|
||||
|
|
@ -82,6 +87,43 @@ function ServerPanel({ settings }: { settings: ServerSettings }) {
|
|||
);
|
||||
}
|
||||
|
||||
function SystemPanel() {
|
||||
const infoQuery = useSystemInfo();
|
||||
const info = infoQuery.data;
|
||||
if (!info) {
|
||||
return <PanelSkeleton />;
|
||||
}
|
||||
return (
|
||||
<Panel title="System">
|
||||
<Row title="Fabro version" help="Version of the running Fabro server.">
|
||||
{info.version ? info.version : <Muted>Unknown</Muted>}
|
||||
</Row>
|
||||
<Row
|
||||
title="Operating system"
|
||||
help="Operating system and CPU architecture reported by the server binary."
|
||||
>
|
||||
<OperatingSystemValue info={info} />
|
||||
</Row>
|
||||
<Row title="Uptime" help="Elapsed time since this server process started.">
|
||||
{info.uptime_secs != null ? (
|
||||
formatDurationSecs(info.uptime_secs)
|
||||
) : (
|
||||
<Muted>Unknown</Muted>
|
||||
)}
|
||||
</Row>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function OperatingSystemValue({ info }: { info: SystemInfoResponse }) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Badge>{info.os ?? "unknown"}</Badge>
|
||||
<Badge>{info.arch ?? "unknown"}</Badge>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ListenValue({ listen }: { listen: ServerListenSettings }) {
|
||||
if (listen.type === "tcp") {
|
||||
return (
|
||||
|
|
|
|||
184
apps/fabro-web/app/routes/settings-resources.test.tsx
Normal file
184
apps/fabro-web/app/routes/settings-resources.test.tsx
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||
import type {
|
||||
SystemCpuResources,
|
||||
SystemDiskResources,
|
||||
SystemMemoryResources,
|
||||
SystemResourcesResponse,
|
||||
} from "@qltysh/fabro-api-client";
|
||||
import TestRenderer, { act } from "react-test-renderer";
|
||||
import { setupReactTestEnv } from "../lib/test-utils";
|
||||
|
||||
let systemResources: SystemResourcesResponse | undefined;
|
||||
let teardownReactTestEnv: (() => void) | undefined;
|
||||
|
||||
mock.module("../lib/queries", () => ({
|
||||
useSystemResources: () => ({ data: systemResources }),
|
||||
}));
|
||||
|
||||
const { default: SettingsResources } = await import("./settings-resources");
|
||||
|
||||
const mountedRenderers: TestRenderer.ReactTestRenderer[] = [];
|
||||
|
||||
function renderSettingsResources() {
|
||||
let renderer: TestRenderer.ReactTestRenderer | undefined;
|
||||
act(() => {
|
||||
renderer = TestRenderer.create(<SettingsResources />);
|
||||
});
|
||||
mountedRenderers.push(renderer!);
|
||||
return renderer!;
|
||||
}
|
||||
|
||||
function textContent(node: ReturnType<TestRenderer.ReactTestRenderer["toJSON"]>): string {
|
||||
if (node == null || typeof node === "boolean") return "";
|
||||
if (typeof node === "string" || typeof node === "number") return String(node);
|
||||
if (Array.isArray(node)) return node.map(textContent).join("");
|
||||
return node.children?.map(textContent).join("") ?? "";
|
||||
}
|
||||
|
||||
type ResourceOverrides = Partial<
|
||||
Omit<SystemResourcesResponse, "cpu" | "memory" | "disk">
|
||||
> & {
|
||||
cpu?: Partial<SystemCpuResources>;
|
||||
memory?: Partial<SystemMemoryResources>;
|
||||
disk?: Partial<SystemDiskResources>;
|
||||
};
|
||||
|
||||
function sampleResources(overrides: ResourceOverrides = {}): SystemResourcesResponse {
|
||||
const resources: SystemResourcesResponse = {
|
||||
sampled_at: "2026-05-20T15:42:10Z",
|
||||
cpu: {
|
||||
supported: true,
|
||||
scope: "server_environment",
|
||||
unavailable_reason: null,
|
||||
logical_cpus: 10,
|
||||
usage_percent: 18.4,
|
||||
sample_window_ms: 5000,
|
||||
},
|
||||
memory: {
|
||||
supported: true,
|
||||
scope: "cgroup",
|
||||
unavailable_reason: null,
|
||||
total_bytes: 8 * 1024 * 1024 * 1024,
|
||||
used_bytes: 3 * 1024 * 1024 * 1024,
|
||||
available_bytes: 5 * 1024 * 1024 * 1024,
|
||||
used_percent: 37.5,
|
||||
host_total_bytes: 32 * 1024 * 1024 * 1024,
|
||||
},
|
||||
disk: {
|
||||
supported: true,
|
||||
scope: "storage_filesystem",
|
||||
unavailable_reason: null,
|
||||
storage_path: "/var/lib/fabro",
|
||||
mount_point: "/",
|
||||
filesystem: "apfs",
|
||||
total_bytes: 500 * 1024 * 1024 * 1024,
|
||||
used_bytes: 200 * 1024 * 1024 * 1024,
|
||||
available_bytes: 300 * 1024 * 1024 * 1024,
|
||||
used_percent: 40,
|
||||
fabro_managed_bytes: 2 * 1024 * 1024 * 1024,
|
||||
fabro_reclaimable_bytes: 512 * 1024 * 1024,
|
||||
},
|
||||
notes: [],
|
||||
};
|
||||
return {
|
||||
sampled_at: overrides.sampled_at ?? resources.sampled_at,
|
||||
cpu: { ...resources.cpu, ...overrides.cpu },
|
||||
memory: { ...resources.memory, ...overrides.memory },
|
||||
disk: { ...resources.disk, ...overrides.disk },
|
||||
notes: overrides.notes ?? resources.notes,
|
||||
};
|
||||
}
|
||||
|
||||
describe("SettingsResources route", () => {
|
||||
beforeEach(() => {
|
||||
teardownReactTestEnv = setupReactTestEnv();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
for (const renderer of mountedRenderers.splice(0)) {
|
||||
renderer.unmount();
|
||||
}
|
||||
});
|
||||
systemResources = undefined;
|
||||
teardownReactTestEnv?.();
|
||||
teardownReactTestEnv = undefined;
|
||||
});
|
||||
|
||||
test("renders loaded resource data", () => {
|
||||
systemResources = sampleResources();
|
||||
|
||||
const renderer = renderSettingsResources();
|
||||
const text = textContent(renderer.toJSON());
|
||||
|
||||
expect(text).toContain("18.4%");
|
||||
expect(text).toContain("5s");
|
||||
expect(text).toContain("3 GiB");
|
||||
expect(text).toContain("8 GiB");
|
||||
expect(text).toContain("2 GiB");
|
||||
expect(text).toContain("512 MiB");
|
||||
});
|
||||
|
||||
test("shows CPU warmup state while usage is null", () => {
|
||||
systemResources = sampleResources({
|
||||
cpu: {
|
||||
supported: true,
|
||||
scope: "server_environment",
|
||||
unavailable_reason: null,
|
||||
logical_cpus: 10,
|
||||
usage_percent: null,
|
||||
sample_window_ms: null,
|
||||
},
|
||||
});
|
||||
|
||||
const renderer = renderSettingsResources();
|
||||
|
||||
expect(textContent(renderer.toJSON())).toContain("Collecting sample");
|
||||
});
|
||||
|
||||
test("renders unsupported resource sections", () => {
|
||||
systemResources = sampleResources({
|
||||
cpu: {
|
||||
supported: false,
|
||||
scope: "server_environment",
|
||||
unavailable_reason: "CPU metrics unavailable",
|
||||
logical_cpus: null,
|
||||
usage_percent: null,
|
||||
sample_window_ms: null,
|
||||
},
|
||||
disk: {
|
||||
supported: false,
|
||||
scope: "storage_filesystem",
|
||||
unavailable_reason: "No storage filesystem matched",
|
||||
storage_path: "/var/lib/fabro",
|
||||
mount_point: null,
|
||||
filesystem: null,
|
||||
total_bytes: null,
|
||||
used_bytes: null,
|
||||
available_bytes: null,
|
||||
used_percent: null,
|
||||
fabro_managed_bytes: 0,
|
||||
fabro_reclaimable_bytes: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const renderer = renderSettingsResources();
|
||||
const text = textContent(renderer.toJSON());
|
||||
|
||||
expect(text).toContain("Unsupported");
|
||||
expect(text).toContain("CPU metrics unavailable");
|
||||
expect(text).toContain("No storage filesystem matched");
|
||||
});
|
||||
|
||||
test("renders notes only when present", () => {
|
||||
systemResources = sampleResources({
|
||||
notes: ["Memory is scoped to the current container."],
|
||||
});
|
||||
|
||||
const renderer = renderSettingsResources();
|
||||
|
||||
expect(textContent(renderer.toJSON())).toContain(
|
||||
"Memory is scoped to the current container.",
|
||||
);
|
||||
});
|
||||
});
|
||||
214
apps/fabro-web/app/routes/settings-resources.tsx
Normal file
214
apps/fabro-web/app/routes/settings-resources.tsx
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import type {
|
||||
SystemCpuResources,
|
||||
SystemDiskResources,
|
||||
SystemMemoryResources,
|
||||
SystemResourcesResponse,
|
||||
} from "@qltysh/fabro-api-client";
|
||||
import { formatBytesAsMemory, formatDurationMs } from "../lib/format";
|
||||
import { useSystemResources } from "../lib/queries";
|
||||
import {
|
||||
Badge,
|
||||
Mono,
|
||||
Muted,
|
||||
Panel,
|
||||
PanelSkeleton,
|
||||
Row,
|
||||
SettingsPageIntro,
|
||||
} from "../components/settings-panel";
|
||||
|
||||
export function meta() {
|
||||
return [{ title: "Resources — Fabro" }];
|
||||
}
|
||||
|
||||
const DESCRIPTION =
|
||||
"Server-visible CPU, memory, and storage filesystem usage for this Fabro process.";
|
||||
|
||||
export default function SettingsResources() {
|
||||
const resourcesQuery = useSystemResources();
|
||||
const resources = resourcesQuery.data;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<SettingsPageIntro description={DESCRIPTION} />
|
||||
{resources ? (
|
||||
<>
|
||||
<CpuPanel cpu={resources.cpu} />
|
||||
<MemoryPanel memory={resources.memory} />
|
||||
<DiskPanel disk={resources.disk} />
|
||||
<NotesPanel resources={resources} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PanelSkeleton />
|
||||
<PanelSkeleton />
|
||||
<PanelSkeleton />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CpuPanel({ cpu }: { cpu: SystemCpuResources }) {
|
||||
if (!cpu.supported) {
|
||||
return (
|
||||
<Panel title="CPU">
|
||||
<UnsupportedRows reason={cpu.unavailable_reason} />
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel title="CPU">
|
||||
<Row title="Usage" help="Delta-based usage for the visible CPU set.">
|
||||
{cpu.usage_percent == null ? (
|
||||
<Muted>Collecting sample</Muted>
|
||||
) : (
|
||||
<UsageMeter percent={cpu.usage_percent} />
|
||||
)}
|
||||
</Row>
|
||||
<Row title="Sample window" help="Elapsed time since the previous CPU sample.">
|
||||
{cpu.sample_window_ms != null ? (
|
||||
formatDurationMs(cpu.sample_window_ms)
|
||||
) : (
|
||||
<Muted>Unknown</Muted>
|
||||
)}
|
||||
</Row>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function MemoryPanel({ memory }: { memory: SystemMemoryResources }) {
|
||||
if (!memory.supported) {
|
||||
return (
|
||||
<Panel title="Memory">
|
||||
<UnsupportedRows reason={memory.unavailable_reason} />
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel title="Memory">
|
||||
<Row title="Usage" help="Memory used within the reported scope.">
|
||||
<UsageMeter
|
||||
percent={memory.used_percent}
|
||||
label={
|
||||
memory.used_bytes != null && memory.total_bytes != null
|
||||
? `${formatBytesAsMemory(memory.used_bytes)} / ${formatBytesAsMemory(memory.total_bytes)}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Row>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function DiskPanel({ disk }: { disk: SystemDiskResources }) {
|
||||
if (!disk.supported) {
|
||||
return (
|
||||
<Panel title="Disk">
|
||||
<UnsupportedRows reason={disk.unavailable_reason} />
|
||||
<Row title="Storage path" help="Configured Fabro storage directory.">
|
||||
<Mono>{disk.storage_path}</Mono>
|
||||
</Row>
|
||||
<FabroStorageRows disk={disk} />
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel title="Disk">
|
||||
<Row title="Usage" help="Storage filesystem capacity used.">
|
||||
<UsageMeter
|
||||
percent={disk.used_percent}
|
||||
label={
|
||||
disk.used_bytes != null && disk.total_bytes != null
|
||||
? `${formatBytesAsMemory(disk.used_bytes)} / ${formatBytesAsMemory(disk.total_bytes)}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Row>
|
||||
<Row title="Mount point" help="Filesystem containing the storage path.">
|
||||
{disk.mount_point ? <Mono>{disk.mount_point}</Mono> : <Muted>Unknown</Muted>}
|
||||
</Row>
|
||||
<FabroStorageRows disk={disk} />
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function FabroStorageRows({ disk }: { disk: SystemDiskResources }) {
|
||||
return (
|
||||
<>
|
||||
<Row title="Fabro managed" help="Bytes currently tracked under Fabro storage.">
|
||||
{formatBytesAsMemory(disk.fabro_managed_bytes)}
|
||||
</Row>
|
||||
<Row title="Reclaimable" help="Bytes Fabro can reclaim by pruning inactive data.">
|
||||
{formatBytesAsMemory(disk.fabro_reclaimable_bytes)}
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function NotesPanel({ resources }: { resources: SystemResourcesResponse }) {
|
||||
const notes = Array.from(new Set(resources.notes));
|
||||
if (notes.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Panel title="Notes">
|
||||
{notes.map((note) => (
|
||||
<Row key={note} title="Note">
|
||||
<span className="text-fg-2">{note}</span>
|
||||
</Row>
|
||||
))}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function UnsupportedRows({ reason }: { reason: string | null }) {
|
||||
return (
|
||||
<>
|
||||
<Row title="Status">
|
||||
<Badge>Unsupported</Badge>
|
||||
</Row>
|
||||
<Row title="Reason">
|
||||
<span className="text-fg-2">{reason ?? "Metric unavailable"}</span>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function UsageMeter({
|
||||
percent,
|
||||
label,
|
||||
}: {
|
||||
percent: number | null | undefined;
|
||||
label?: string;
|
||||
}) {
|
||||
const safePercent = percent == null ? null : Math.min(100, Math.max(0, percent));
|
||||
const value = safePercent == null ? "Not available" : formatPercent(safePercent);
|
||||
return (
|
||||
<div className="min-w-0 space-y-1.5">
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
<span className="truncate text-sm text-fg">{label ?? value}</span>
|
||||
{label ? (
|
||||
<span className="font-mono text-xs tabular-nums text-fg-muted">{value}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
role="meter"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={safePercent ?? undefined}
|
||||
className="h-2 overflow-hidden rounded-sm bg-overlay-strong"
|
||||
>
|
||||
<div
|
||||
className="h-full rounded-sm bg-teal-500 transition-[width]"
|
||||
style={{ width: `${safePercent ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatPercent(value: number) {
|
||||
return `${Number.isInteger(value) ? value : value.toFixed(1)}%`;
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import {
|
|||
Cog6ToothIcon,
|
||||
CpuChipIcon,
|
||||
PuzzlePieceIcon,
|
||||
ServerStackIcon,
|
||||
ShieldCheckIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Link, Outlet, useLocation, useMatches } from "react-router";
|
||||
|
|
@ -57,6 +58,12 @@ const navItems: NavEntry[] = [
|
|||
icon: CircleStackIcon,
|
||||
match: (p) => p.startsWith("/settings/storage"),
|
||||
},
|
||||
{
|
||||
name: "Resources",
|
||||
href: "/settings/resources",
|
||||
icon: ServerStackIcon,
|
||||
match: (p) => p.startsWith("/settings/resources"),
|
||||
},
|
||||
{ type: "divider", key: "after-storage" },
|
||||
{
|
||||
name: "Live Events",
|
||||
|
|
|
|||
|
|
@ -3620,6 +3620,20 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/SystemInfoResponse"
|
||||
|
||||
/api/v1/system/resources:
|
||||
get:
|
||||
operationId: getSystemResources
|
||||
tags: [System]
|
||||
summary: Retrieve System Resources
|
||||
description: Returns server-visible CPU, memory, and storage filesystem resource usage.
|
||||
responses:
|
||||
"200":
|
||||
description: System resource usage
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SystemResourcesResponse"
|
||||
|
||||
/api/v1/system/df:
|
||||
get:
|
||||
operationId: getSystemDiskUsage
|
||||
|
|
@ -10246,6 +10260,175 @@ components:
|
|||
format: int64
|
||||
description: Runs currently queued or executing.
|
||||
|
||||
SystemResourcesResponse:
|
||||
description: Server-visible runtime resource usage for the active Fabro process environment.
|
||||
type: object
|
||||
required: [sampled_at, cpu, memory, disk, notes]
|
||||
properties:
|
||||
sampled_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Timestamp when the sample was collected.
|
||||
cpu:
|
||||
$ref: "#/components/schemas/SystemCpuResources"
|
||||
memory:
|
||||
$ref: "#/components/schemas/SystemMemoryResources"
|
||||
disk:
|
||||
$ref: "#/components/schemas/SystemDiskResources"
|
||||
notes:
|
||||
type: array
|
||||
description: Human-readable caveats about unavailable or scoped metrics.
|
||||
items:
|
||||
type: string
|
||||
|
||||
SystemCpuResources:
|
||||
description: CPU resources visible to the Fabro server process.
|
||||
type: object
|
||||
required:
|
||||
- supported
|
||||
- scope
|
||||
- unavailable_reason
|
||||
- logical_cpus
|
||||
- usage_percent
|
||||
- sample_window_ms
|
||||
properties:
|
||||
supported:
|
||||
type: boolean
|
||||
description: Whether CPU metrics are available on this platform.
|
||||
scope:
|
||||
$ref: "#/components/schemas/SystemCpuResourceScope"
|
||||
unavailable_reason:
|
||||
type: ["string", "null"]
|
||||
description: Reason metrics are unavailable when unsupported.
|
||||
logical_cpus:
|
||||
type: ["integer", "null"]
|
||||
format: int64
|
||||
description: Logical CPUs visible to the server process.
|
||||
usage_percent:
|
||||
type: ["number", "null"]
|
||||
format: double
|
||||
description: CPU usage percentage, null until a delta sample is available.
|
||||
sample_window_ms:
|
||||
type: ["integer", "null"]
|
||||
format: int64
|
||||
description: Elapsed milliseconds since the previous CPU sample, null until a delta sample is available.
|
||||
|
||||
SystemCpuResourceScope:
|
||||
description: Scope for CPU resource metrics.
|
||||
type: string
|
||||
enum: [server_environment]
|
||||
|
||||
SystemMemoryResources:
|
||||
description: Memory resources visible to the Fabro server process.
|
||||
type: object
|
||||
required:
|
||||
- supported
|
||||
- scope
|
||||
- unavailable_reason
|
||||
- total_bytes
|
||||
- used_bytes
|
||||
- available_bytes
|
||||
- used_percent
|
||||
- host_total_bytes
|
||||
properties:
|
||||
supported:
|
||||
type: boolean
|
||||
description: Whether memory metrics are available on this platform.
|
||||
scope:
|
||||
$ref: "#/components/schemas/SystemMemoryResourceScope"
|
||||
unavailable_reason:
|
||||
type: ["string", "null"]
|
||||
description: Reason metrics are unavailable when unsupported.
|
||||
total_bytes:
|
||||
type: ["integer", "null"]
|
||||
format: int64
|
||||
description: Total memory for the reported scope.
|
||||
used_bytes:
|
||||
type: ["integer", "null"]
|
||||
format: int64
|
||||
description: Used memory for the reported scope.
|
||||
available_bytes:
|
||||
type: ["integer", "null"]
|
||||
format: int64
|
||||
description: Available memory for the reported scope.
|
||||
used_percent:
|
||||
type: ["number", "null"]
|
||||
format: double
|
||||
description: Used memory percentage for the reported scope.
|
||||
host_total_bytes:
|
||||
type: ["integer", "null"]
|
||||
format: int64
|
||||
description: Host total memory, included even when reporting a cgroup scope.
|
||||
|
||||
SystemMemoryResourceScope:
|
||||
description: "`cgroup` when Linux cgroup limits are available, otherwise `host`."
|
||||
type: string
|
||||
enum: [host, cgroup]
|
||||
|
||||
SystemDiskResources:
|
||||
description: Filesystem resources for the configured Fabro storage root.
|
||||
type: object
|
||||
required:
|
||||
- supported
|
||||
- scope
|
||||
- unavailable_reason
|
||||
- storage_path
|
||||
- mount_point
|
||||
- filesystem
|
||||
- total_bytes
|
||||
- used_bytes
|
||||
- available_bytes
|
||||
- used_percent
|
||||
- fabro_managed_bytes
|
||||
- fabro_reclaimable_bytes
|
||||
properties:
|
||||
supported:
|
||||
type: boolean
|
||||
description: Whether storage filesystem metrics are available.
|
||||
scope:
|
||||
$ref: "#/components/schemas/SystemDiskResourceScope"
|
||||
unavailable_reason:
|
||||
type: ["string", "null"]
|
||||
description: Reason metrics are unavailable when unsupported.
|
||||
storage_path:
|
||||
type: string
|
||||
description: Configured Fabro storage directory.
|
||||
mount_point:
|
||||
type: ["string", "null"]
|
||||
description: Mount point for the filesystem containing the storage directory.
|
||||
filesystem:
|
||||
type: ["string", "null"]
|
||||
description: Filesystem name reported by the operating system.
|
||||
total_bytes:
|
||||
type: ["integer", "null"]
|
||||
format: int64
|
||||
description: Total bytes on the storage filesystem.
|
||||
used_bytes:
|
||||
type: ["integer", "null"]
|
||||
format: int64
|
||||
description: Used bytes on the storage filesystem.
|
||||
available_bytes:
|
||||
type: ["integer", "null"]
|
||||
format: int64
|
||||
description: Available bytes on the storage filesystem.
|
||||
used_percent:
|
||||
type: ["number", "null"]
|
||||
format: double
|
||||
description: Used percentage on the storage filesystem.
|
||||
fabro_managed_bytes:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Bytes managed by Fabro under the storage root.
|
||||
fabro_reclaimable_bytes:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Bytes reclaimable through Fabro pruning.
|
||||
|
||||
SystemDiskResourceScope:
|
||||
description: Scope for disk resource metrics.
|
||||
type: string
|
||||
enum: [storage_filesystem]
|
||||
|
||||
SystemRepairRunsResponse:
|
||||
description: Runs that need manual repair or deletion because they cannot be loaded.
|
||||
type: object
|
||||
|
|
|
|||
BIN
docs/public/images/web/settings-resources.jpg
Normal file
BIN
docs/public/images/web/settings-resources.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
|
|
@ -93,6 +93,7 @@ percent-encoding.workspace = true
|
|||
url = "2"
|
||||
zeroize.workspace = true
|
||||
strum.workspace = true
|
||||
sysinfo = { version = "0.35.2", default-features = false, features = ["system", "disk"] }
|
||||
|
||||
[build-dependencies]
|
||||
fabro-build-support = { path = "../build-support" }
|
||||
|
|
|
|||
|
|
@ -882,6 +882,52 @@ pub(crate) async fn get_system_info(
|
|||
.into_response()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_system_resources(
|
||||
_auth: RequiredUser,
|
||||
State(_state): State<Arc<AppState>>,
|
||||
) -> Response {
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"sampled_at": "2026-05-20T15:42:10Z",
|
||||
"cpu": {
|
||||
"supported": true,
|
||||
"scope": "server_environment",
|
||||
"unavailable_reason": null,
|
||||
"logical_cpus": 8,
|
||||
"usage_percent": 18.4,
|
||||
"sample_window_ms": 5000
|
||||
},
|
||||
"memory": {
|
||||
"supported": true,
|
||||
"scope": "host",
|
||||
"unavailable_reason": null,
|
||||
"total_bytes": 17179869184_i64,
|
||||
"used_bytes": 6442450944_i64,
|
||||
"available_bytes": 10737418240_i64,
|
||||
"used_percent": 37.5,
|
||||
"host_total_bytes": 17179869184_i64
|
||||
},
|
||||
"disk": {
|
||||
"supported": true,
|
||||
"scope": "storage_filesystem",
|
||||
"unavailable_reason": null,
|
||||
"storage_path": "/demo/fabro/storage",
|
||||
"mount_point": "/",
|
||||
"filesystem": "demo-fs",
|
||||
"total_bytes": 536870912000_i64,
|
||||
"used_bytes": 214748364800_i64,
|
||||
"available_bytes": 322122547200_i64,
|
||||
"used_percent": 40.0,
|
||||
"fabro_managed_bytes": 1280,
|
||||
"fabro_reclaimable_bytes": 1280
|
||||
},
|
||||
"notes": []
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_system_disk_usage(
|
||||
_auth: RequiredUser,
|
||||
State(_state): State<Arc<AppState>>,
|
||||
|
|
|
|||
|
|
@ -36,9 +36,11 @@ pub use fabro_api::types::{
|
|||
RunArtifactListResponse, RunBilling, RunBillingStage, RunBillingTotals, RunError, RunManifest,
|
||||
RunStage, SandboxDetails, SandboxFileEntry, SandboxFileListResponse, SandboxService,
|
||||
SandboxServiceListResponse, SshAccessRequest, SshAccessResponse, StageHandler, StageState,
|
||||
StartRunRequest, SubmitAnswerRequest, SystemFeatures, SystemInfoResponse, SystemRepairRunIssue,
|
||||
SystemRepairRunsResponse, SystemRunCounts, TimelineEntryResponse, VncPreviewResponse,
|
||||
WriteBlobResponse,
|
||||
StartRunRequest, SubmitAnswerRequest, SystemCpuResourceScope, SystemCpuResources,
|
||||
SystemDiskResourceScope, SystemDiskResources, SystemFeatures, SystemInfoResponse,
|
||||
SystemMemoryResourceScope, SystemMemoryResources, SystemRepairRunIssue,
|
||||
SystemRepairRunsResponse, SystemResourcesResponse, SystemRunCounts, TimelineEntryResponse,
|
||||
VncPreviewResponse, WriteBlobResponse,
|
||||
};
|
||||
use fabro_auth::{CredentialSource, VaultCredentialSource, auth_issue_message};
|
||||
#[cfg(test)]
|
||||
|
|
@ -142,6 +144,7 @@ use crate::{
|
|||
};
|
||||
|
||||
mod handler;
|
||||
mod resource_sampler;
|
||||
mod session_runtime;
|
||||
|
||||
pub(crate) use handler::events::EventListParams;
|
||||
|
|
@ -519,6 +522,7 @@ pub struct AppState {
|
|||
artifact_store: ArtifactStore,
|
||||
worker_tokens: WorkerTokenKeys,
|
||||
started_at: Instant,
|
||||
resource_sampler: resource_sampler::ResourceSampler,
|
||||
max_concurrent_runs: usize,
|
||||
scheduler_notify: Notify,
|
||||
global_event_tx: broadcast::Sender<EventEnvelope>,
|
||||
|
|
@ -1646,6 +1650,7 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
|
|||
artifact_store,
|
||||
worker_tokens,
|
||||
started_at: Instant::now(),
|
||||
resource_sampler: resource_sampler::ResourceSampler::new(),
|
||||
max_concurrent_runs,
|
||||
scheduler_notify: Notify::new(),
|
||||
global_event_tx,
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ pub(super) fn demo_routes() -> Router<Arc<AppState>> {
|
|||
.route("/health/diagnostics", post(demo::run_diagnostics))
|
||||
.route("/settings", get(demo::get_server_settings))
|
||||
.route("/system/info", get(demo::get_system_info))
|
||||
.route("/system/resources", get(demo::get_system_resources))
|
||||
.route("/system/df", get(demo::get_system_disk_usage))
|
||||
.route("/system/repair/runs", get(demo::get_system_repair_runs))
|
||||
.route("/system/prune/runs", post(demo::prune_runs))
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ use super::super::{
|
|||
PruneRunsRequest, PruneRunsResponse, Query, RequiredUser, Response, Router, RunStatus, State,
|
||||
StatusCode, SystemInfoResponse, SystemRepairRunIssue, SystemRepairRunsResponse,
|
||||
SystemRunCounts, build_disk_usage_response, build_prune_plan, delete_run_internal, diagnostics,
|
||||
get, post, resolve_interp_string, spawn_blocking, system_features, system_sandbox_provider,
|
||||
to_i64,
|
||||
get, post, resolve_interp_string, resource_sampler, spawn_blocking, system_features,
|
||||
system_sandbox_provider, to_i64,
|
||||
};
|
||||
|
||||
pub(super) fn routes() -> Router<Arc<AppState>> {
|
||||
|
|
@ -17,6 +17,7 @@ pub(super) fn routes() -> Router<Arc<AppState>> {
|
|||
.route("/health/diagnostics", post(run_diagnostics))
|
||||
.route("/settings", get(get_server_settings))
|
||||
.route("/system/info", get(get_system_info))
|
||||
.route("/system/resources", get(get_system_resources))
|
||||
.route("/system/df", get(get_system_df))
|
||||
.route("/system/repair/runs", get(get_system_repair_runs))
|
||||
.route("/system/prune/runs", post(prune_runs))
|
||||
|
|
@ -83,6 +84,15 @@ async fn get_system_info(_auth: RequiredUser, State(state): State<Arc<AppState>>
|
|||
(StatusCode::OK, Json(response)).into_response()
|
||||
}
|
||||
|
||||
async fn get_system_resources(_auth: RequiredUser, State(state): State<Arc<AppState>>) -> Response {
|
||||
match resource_sampler::sample_system_resources(&state).await {
|
||||
Ok(response) => (StatusCode::OK, Json(response)).into_response(),
|
||||
Err(err) => {
|
||||
ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_system_df(
|
||||
_auth: RequiredUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
|
|
|
|||
423
lib/crates/fabro-server/src/server/resource_sampler.rs
Normal file
423
lib/crates/fabro-server/src/server/resource_sampler.rs
Normal file
|
|
@ -0,0 +1,423 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use sysinfo::{Disks, System};
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
use super::{
|
||||
AppState, SystemCpuResourceScope, SystemCpuResources, SystemDiskResourceScope,
|
||||
SystemDiskResources, SystemMemoryResourceScope, SystemMemoryResources, SystemResourcesResponse,
|
||||
build_disk_usage_response, to_i64,
|
||||
};
|
||||
|
||||
const FABRO_STORAGE_USAGE_CACHE_TTL: Duration = Duration::from_mins(1);
|
||||
|
||||
pub(in crate::server) struct ResourceSampler {
|
||||
system: Mutex<SystemSamplerState>,
|
||||
fabro_storage_usage: AsyncMutex<Option<CachedFabroStorageUsage>>,
|
||||
}
|
||||
|
||||
struct SystemSamplerState {
|
||||
system: System,
|
||||
last_cpu_sample_at: Option<Instant>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct CachedFabroStorageUsage {
|
||||
sampled_at: Instant,
|
||||
usage: FabroStorageUsage,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct FabroStorageUsage {
|
||||
managed_bytes: i64,
|
||||
reclaimable_bytes: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct CgroupMemory {
|
||||
total_bytes: u64,
|
||||
available_bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct MemorySelection {
|
||||
scope: SystemMemoryResourceScope,
|
||||
total_bytes: u64,
|
||||
used_bytes: u64,
|
||||
available_bytes: u64,
|
||||
host_total_bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct DiskCandidate {
|
||||
mount_point: PathBuf,
|
||||
filesystem: String,
|
||||
total_bytes: u64,
|
||||
available_bytes: u64,
|
||||
}
|
||||
|
||||
impl ResourceSampler {
|
||||
pub(in crate::server) fn new() -> Self {
|
||||
Self {
|
||||
system: Mutex::new(SystemSamplerState {
|
||||
system: System::new(),
|
||||
last_cpu_sample_at: None,
|
||||
}),
|
||||
fabro_storage_usage: AsyncMutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_cpu_and_memory(&self) -> (SystemCpuResources, SystemMemoryResources) {
|
||||
let mut state = self.system.lock().expect("resource sampler lock poisoned");
|
||||
let sampled_at = Instant::now();
|
||||
let sample_window_ms = state
|
||||
.last_cpu_sample_at
|
||||
.map(|last_sampled_at| to_i64(sampled_at.duration_since(last_sampled_at).as_millis()));
|
||||
|
||||
state.system.refresh_cpu_usage();
|
||||
state.system.refresh_memory();
|
||||
|
||||
let logical_cpus = logical_cpu_count(&state.system);
|
||||
let usage_percent = sample_window_ms
|
||||
.is_some()
|
||||
.then(|| round_one(f64::from(state.system.global_cpu_usage())));
|
||||
state.last_cpu_sample_at = Some(sampled_at);
|
||||
|
||||
let cpu = if sysinfo::IS_SUPPORTED_SYSTEM {
|
||||
SystemCpuResources {
|
||||
supported: true,
|
||||
scope: SystemCpuResourceScope::ServerEnvironment,
|
||||
unavailable_reason: None,
|
||||
logical_cpus: Some(to_i64(logical_cpus)),
|
||||
usage_percent,
|
||||
sample_window_ms,
|
||||
}
|
||||
} else {
|
||||
SystemCpuResources {
|
||||
supported: false,
|
||||
scope: SystemCpuResourceScope::ServerEnvironment,
|
||||
unavailable_reason: Some(
|
||||
"system metrics are not supported on this platform".to_string(),
|
||||
),
|
||||
logical_cpus: None,
|
||||
usage_percent: None,
|
||||
sample_window_ms: None,
|
||||
}
|
||||
};
|
||||
|
||||
let cgroup = state.system.cgroup_limits().map(|limits| CgroupMemory {
|
||||
total_bytes: limits.total_memory,
|
||||
available_bytes: limits.free_memory,
|
||||
});
|
||||
let memory = memory_response(select_memory(
|
||||
state.system.total_memory(),
|
||||
state.system.used_memory(),
|
||||
state.system.available_memory(),
|
||||
cgroup,
|
||||
));
|
||||
|
||||
(cpu, memory)
|
||||
}
|
||||
|
||||
async fn sample_fabro_storage_usage(
|
||||
&self,
|
||||
state: &AppState,
|
||||
storage_path: &Path,
|
||||
) -> anyhow::Result<FabroStorageUsage> {
|
||||
let mut cached = self.fabro_storage_usage.lock().await;
|
||||
if let Some(cached) = cached
|
||||
.as_ref()
|
||||
.filter(|cached| cached.sampled_at.elapsed() < FABRO_STORAGE_USAGE_CACHE_TTL)
|
||||
{
|
||||
return Ok(cached.usage);
|
||||
}
|
||||
|
||||
let summaries = state
|
||||
.store
|
||||
.list_runs(&fabro_store::ListRunsQuery::default())
|
||||
.await
|
||||
.context("failed to list runs for resource sampling")?;
|
||||
let storage_path = storage_path.to_path_buf();
|
||||
let usage = spawn_blocking(move || compute_fabro_storage_usage(&summaries, &storage_path))
|
||||
.await
|
||||
.context("resource storage usage task failed")??;
|
||||
|
||||
*cached = Some(CachedFabroStorageUsage {
|
||||
sampled_at: Instant::now(),
|
||||
usage,
|
||||
});
|
||||
Ok(usage)
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::server) async fn sample_system_resources(
|
||||
state: &AppState,
|
||||
) -> anyhow::Result<SystemResourcesResponse> {
|
||||
let sampled_at = chrono::Utc::now();
|
||||
let (cpu, memory) = state.resource_sampler.sample_cpu_and_memory();
|
||||
let storage_path = state.server_storage_dir();
|
||||
let fabro_usage = state
|
||||
.resource_sampler
|
||||
.sample_fabro_storage_usage(state, &storage_path)
|
||||
.await
|
||||
.context("failed to sample Fabro-managed storage usage")?;
|
||||
|
||||
let disk = sample_disk_resources(&storage_path, fabro_usage);
|
||||
|
||||
Ok(SystemResourcesResponse {
|
||||
sampled_at,
|
||||
cpu,
|
||||
memory,
|
||||
disk,
|
||||
notes: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn logical_cpu_count(system: &System) -> usize {
|
||||
let sysinfo_count = system.cpus().len();
|
||||
if sysinfo_count > 0 {
|
||||
return sysinfo_count;
|
||||
}
|
||||
std::thread::available_parallelism().map_or(0, std::num::NonZeroUsize::get)
|
||||
}
|
||||
|
||||
fn memory_response(selection: Option<MemorySelection>) -> SystemMemoryResources {
|
||||
let Some(selection) = selection else {
|
||||
return SystemMemoryResources {
|
||||
supported: false,
|
||||
scope: SystemMemoryResourceScope::Host,
|
||||
unavailable_reason: Some("memory metrics reported zero total bytes".to_string()),
|
||||
total_bytes: None,
|
||||
used_bytes: None,
|
||||
available_bytes: None,
|
||||
used_percent: None,
|
||||
host_total_bytes: None,
|
||||
};
|
||||
};
|
||||
|
||||
SystemMemoryResources {
|
||||
supported: true,
|
||||
scope: selection.scope,
|
||||
unavailable_reason: None,
|
||||
total_bytes: Some(to_i64(selection.total_bytes)),
|
||||
used_bytes: Some(to_i64(selection.used_bytes)),
|
||||
available_bytes: Some(to_i64(selection.available_bytes)),
|
||||
used_percent: percent(selection.used_bytes, selection.total_bytes),
|
||||
host_total_bytes: Some(to_i64(selection.host_total_bytes)),
|
||||
}
|
||||
}
|
||||
|
||||
fn select_memory(
|
||||
host_total_bytes: u64,
|
||||
host_used_bytes: u64,
|
||||
host_available_bytes: u64,
|
||||
cgroup: Option<CgroupMemory>,
|
||||
) -> Option<MemorySelection> {
|
||||
if let Some(cgroup) = cgroup.filter(|cgroup| cgroup.total_bytes > 0) {
|
||||
let available_bytes = cgroup.available_bytes.min(cgroup.total_bytes);
|
||||
let used_bytes = cgroup.total_bytes.saturating_sub(available_bytes);
|
||||
return Some(MemorySelection {
|
||||
scope: SystemMemoryResourceScope::Cgroup,
|
||||
total_bytes: cgroup.total_bytes,
|
||||
used_bytes,
|
||||
available_bytes,
|
||||
host_total_bytes,
|
||||
});
|
||||
}
|
||||
|
||||
if host_total_bytes == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(MemorySelection {
|
||||
scope: SystemMemoryResourceScope::Host,
|
||||
total_bytes: host_total_bytes,
|
||||
used_bytes: host_used_bytes.min(host_total_bytes),
|
||||
available_bytes: host_available_bytes.min(host_total_bytes),
|
||||
host_total_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
fn compute_fabro_storage_usage(
|
||||
summaries: &[fabro_types::Run],
|
||||
storage_path: &Path,
|
||||
) -> anyhow::Result<FabroStorageUsage> {
|
||||
let usage = build_disk_usage_response(summaries, storage_path, false)?;
|
||||
Ok(FabroStorageUsage {
|
||||
managed_bytes: usage.total_size_bytes.unwrap_or_default(),
|
||||
reclaimable_bytes: usage.total_reclaimable_bytes.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn sample_disk_resources(
|
||||
storage_path: &Path,
|
||||
fabro_usage: FabroStorageUsage,
|
||||
) -> SystemDiskResources {
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
let candidates = disks
|
||||
.list()
|
||||
.iter()
|
||||
.map(|disk| DiskCandidate {
|
||||
mount_point: disk.mount_point().to_path_buf(),
|
||||
filesystem: disk.file_system().to_string_lossy().to_string(),
|
||||
total_bytes: disk.total_space(),
|
||||
available_bytes: disk.available_space(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let Some(disk) = select_storage_disk(storage_path, &candidates) else {
|
||||
return SystemDiskResources {
|
||||
supported: false,
|
||||
scope: SystemDiskResourceScope::StorageFilesystem,
|
||||
unavailable_reason: Some(format!(
|
||||
"no filesystem mount matched storage path {}",
|
||||
storage_path.display()
|
||||
)),
|
||||
storage_path: storage_path.display().to_string(),
|
||||
mount_point: None,
|
||||
filesystem: None,
|
||||
total_bytes: None,
|
||||
used_bytes: None,
|
||||
available_bytes: None,
|
||||
used_percent: None,
|
||||
fabro_managed_bytes: fabro_usage.managed_bytes,
|
||||
fabro_reclaimable_bytes: fabro_usage.reclaimable_bytes,
|
||||
};
|
||||
};
|
||||
|
||||
if disk.total_bytes == 0 {
|
||||
return SystemDiskResources {
|
||||
supported: false,
|
||||
scope: SystemDiskResourceScope::StorageFilesystem,
|
||||
unavailable_reason: Some(format!(
|
||||
"filesystem {} reported zero total bytes",
|
||||
disk.mount_point.display()
|
||||
)),
|
||||
storage_path: storage_path.display().to_string(),
|
||||
mount_point: Some(disk.mount_point.display().to_string()),
|
||||
filesystem: Some(disk.filesystem.clone()),
|
||||
total_bytes: None,
|
||||
used_bytes: None,
|
||||
available_bytes: None,
|
||||
used_percent: None,
|
||||
fabro_managed_bytes: fabro_usage.managed_bytes,
|
||||
fabro_reclaimable_bytes: fabro_usage.reclaimable_bytes,
|
||||
};
|
||||
}
|
||||
|
||||
let available_bytes = disk.available_bytes.min(disk.total_bytes);
|
||||
let used_bytes = disk.total_bytes.saturating_sub(available_bytes);
|
||||
|
||||
SystemDiskResources {
|
||||
supported: true,
|
||||
scope: SystemDiskResourceScope::StorageFilesystem,
|
||||
unavailable_reason: None,
|
||||
storage_path: storage_path.display().to_string(),
|
||||
mount_point: Some(disk.mount_point.display().to_string()),
|
||||
filesystem: Some(disk.filesystem.clone()),
|
||||
total_bytes: Some(to_i64(disk.total_bytes)),
|
||||
used_bytes: Some(to_i64(used_bytes)),
|
||||
available_bytes: Some(to_i64(available_bytes)),
|
||||
used_percent: percent(used_bytes, disk.total_bytes),
|
||||
fabro_managed_bytes: fabro_usage.managed_bytes,
|
||||
fabro_reclaimable_bytes: fabro_usage.reclaimable_bytes,
|
||||
}
|
||||
}
|
||||
|
||||
fn select_storage_disk<'a>(
|
||||
storage_path: &Path,
|
||||
disks: &'a [DiskCandidate],
|
||||
) -> Option<&'a DiskCandidate> {
|
||||
disks
|
||||
.iter()
|
||||
.filter(|disk| storage_path.starts_with(&disk.mount_point))
|
||||
.max_by_key(|disk| disk.mount_point.components().count())
|
||||
}
|
||||
|
||||
fn percent(used: u64, total: u64) -> Option<f64> {
|
||||
if total == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(round_one((used as f64 / total as f64) * 100.0))
|
||||
}
|
||||
|
||||
fn round_one(value: f64) -> f64 {
|
||||
(value * 10.0).round() / 10.0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use super::{
|
||||
CgroupMemory, DiskCandidate, SystemMemoryResourceScope, percent, select_memory,
|
||||
select_storage_disk,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn percent_returns_one_decimal_percentage() {
|
||||
assert_eq!(percent(1, 3), Some(33.3));
|
||||
assert_eq!(percent(0, 10), Some(0.0));
|
||||
assert_eq!(percent(1, 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_memory_uses_host_values_without_cgroup_limits() {
|
||||
let selection =
|
||||
select_memory(1_000, 400, 600, None).expect("host memory should be selected");
|
||||
|
||||
assert_eq!(selection.scope, SystemMemoryResourceScope::Host);
|
||||
assert_eq!(selection.total_bytes, 1_000);
|
||||
assert_eq!(selection.used_bytes, 400);
|
||||
assert_eq!(selection.available_bytes, 600);
|
||||
assert_eq!(selection.host_total_bytes, 1_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_memory_prefers_cgroup_limits_when_available() {
|
||||
let selection = select_memory(
|
||||
1_000,
|
||||
200,
|
||||
800,
|
||||
Some(CgroupMemory {
|
||||
total_bytes: 500,
|
||||
available_bytes: 125,
|
||||
}),
|
||||
)
|
||||
.expect("cgroup memory should be selected");
|
||||
|
||||
assert_eq!(selection.scope, SystemMemoryResourceScope::Cgroup);
|
||||
assert_eq!(selection.total_bytes, 500);
|
||||
assert_eq!(selection.used_bytes, 375);
|
||||
assert_eq!(selection.available_bytes, 125);
|
||||
assert_eq!(selection.host_total_bytes, 1_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_storage_disk_uses_longest_mount_point_prefix() {
|
||||
let disks = vec![
|
||||
disk("/"),
|
||||
disk("/var"),
|
||||
disk("/var/lib"),
|
||||
disk("/var/lib-other"),
|
||||
];
|
||||
|
||||
let selected = select_storage_disk(Path::new("/var/lib/fabro/runs"), &disks)
|
||||
.expect("storage disk should match");
|
||||
|
||||
assert_eq!(selected.mount_point, Path::new("/var/lib"));
|
||||
}
|
||||
|
||||
fn disk(mount_point: &str) -> DiskCandidate {
|
||||
DiskCandidate {
|
||||
mount_point: Path::new(mount_point).to_path_buf(),
|
||||
filesystem: "testfs".to_string(),
|
||||
total_bytes: 1_000,
|
||||
available_bytes: 500,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -144,6 +144,97 @@ async fn get_system_info_returns_runtime_fields() {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_system_resources_returns_server_visible_metrics() {
|
||||
let (_temp, settings, expected_storage_dir) = temp_storage_settings();
|
||||
let app =
|
||||
fabro_server::test_support::build_test_router(test_app_state_with_options(settings, 5));
|
||||
|
||||
let request = Request::builder()
|
||||
.method("GET")
|
||||
.uri(api("/system/resources"))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let response = app.oneshot(request).await.unwrap();
|
||||
|
||||
let body = response_json(response, StatusCode::OK, "GET /api/v1/system/resources").await;
|
||||
let sampled_at = body["sampled_at"]
|
||||
.as_str()
|
||||
.expect("sampled_at should be an RFC3339 timestamp");
|
||||
chrono::DateTime::parse_from_rfc3339(sampled_at).expect("sampled_at should parse as RFC3339");
|
||||
|
||||
assert_eq!(body["cpu"]["supported"], true);
|
||||
assert_eq!(body["cpu"]["scope"], "server_environment");
|
||||
assert!(
|
||||
body["cpu"]["sample_window_ms"].is_null()
|
||||
|| body["cpu"]["sample_window_ms"]
|
||||
.as_i64()
|
||||
.is_some_and(|value| value >= 0),
|
||||
"sample window should be null until a delta sample is available or nonnegative: {body}"
|
||||
);
|
||||
assert!(
|
||||
body["cpu"]["logical_cpus"].as_i64().unwrap_or_default() > 0,
|
||||
"logical CPU count should be positive"
|
||||
);
|
||||
assert_percent_if_present(&body["cpu"]["usage_percent"]);
|
||||
|
||||
assert_eq!(body["memory"]["supported"], true);
|
||||
assert!(matches!(
|
||||
body["memory"]["scope"].as_str(),
|
||||
Some("host" | "cgroup")
|
||||
));
|
||||
assert_nonnegative_i64(&body["memory"]["total_bytes"], "memory.total_bytes");
|
||||
assert_nonnegative_i64(&body["memory"]["used_bytes"], "memory.used_bytes");
|
||||
assert_nonnegative_i64(&body["memory"]["available_bytes"], "memory.available_bytes");
|
||||
assert_percent_if_present(&body["memory"]["used_percent"]);
|
||||
|
||||
assert_eq!(body["disk"]["supported"], true);
|
||||
assert_eq!(body["disk"]["scope"], "storage_filesystem");
|
||||
assert_eq!(
|
||||
body["disk"]["storage_path"],
|
||||
expected_storage_dir.display().to_string()
|
||||
);
|
||||
assert!(body["disk"]["mount_point"].as_str().is_some());
|
||||
assert_nonnegative_i64(&body["disk"]["total_bytes"], "disk.total_bytes");
|
||||
assert_nonnegative_i64(&body["disk"]["used_bytes"], "disk.used_bytes");
|
||||
assert_nonnegative_i64(&body["disk"]["available_bytes"], "disk.available_bytes");
|
||||
assert_percent_if_present(&body["disk"]["used_percent"]);
|
||||
assert_nonnegative_i64(
|
||||
&body["disk"]["fabro_managed_bytes"],
|
||||
"disk.fabro_managed_bytes",
|
||||
);
|
||||
assert_nonnegative_i64(
|
||||
&body["disk"]["fabro_reclaimable_bytes"],
|
||||
"disk.fabro_reclaimable_bytes",
|
||||
);
|
||||
assert!(
|
||||
body["notes"]
|
||||
.as_array()
|
||||
.is_some_and(std::vec::Vec::is_empty),
|
||||
"initial test app resources should not need notes: {body}"
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_nonnegative_i64(value: &serde_json::Value, name: &str) {
|
||||
assert!(
|
||||
value.as_i64().is_some_and(|value| value >= 0),
|
||||
"{name} should be a nonnegative integer: {value:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_percent_if_present(value: &serde_json::Value) {
|
||||
if value.is_null() {
|
||||
return;
|
||||
}
|
||||
let percent = value
|
||||
.as_f64()
|
||||
.expect("percent should be numeric when present");
|
||||
assert!(
|
||||
(0.0..=100.0).contains(&percent),
|
||||
"percent should be between 0 and 100: {percent}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_app_state_with_options_respects_max_concurrent_runs() {
|
||||
let app = test_app_with_scheduler(test_app_state_with_options(test_settings(), 1));
|
||||
|
|
|
|||
|
|
@ -378,10 +378,17 @@ models/submit-answer-yes-request.ts
|
|||
models/submit-turn-request.ts
|
||||
models/success-reason.ts
|
||||
models/system-actor-kind.ts
|
||||
models/system-cpu-resource-scope.ts
|
||||
models/system-cpu-resources.ts
|
||||
models/system-disk-resource-scope.ts
|
||||
models/system-disk-resources.ts
|
||||
models/system-features.ts
|
||||
models/system-info-response.ts
|
||||
models/system-memory-resource-scope.ts
|
||||
models/system-memory-resources.ts
|
||||
models/system-repair-run-issue.ts
|
||||
models/system-repair-runs-response.ts
|
||||
models/system-resources-response.ts
|
||||
models/system-run-counts.ts
|
||||
models/timeline-entry-response.ts
|
||||
models/tls-mode.ts
|
||||
|
|
|
|||
69
lib/packages/fabro-api-client/src/api/system-api.ts
generated
69
lib/packages/fabro-api-client/src/api/system-api.ts
generated
|
|
@ -33,6 +33,8 @@ import type { PruneRunsResponse } from '../models';
|
|||
import type { SystemInfoResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { SystemRepairRunsResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { SystemResourcesResponse } from '../models';
|
||||
/**
|
||||
* SystemApi - axios parameter creator
|
||||
*/
|
||||
|
|
@ -192,6 +194,42 @@ export const SystemApiAxiosParamCreator = function (configuration?: Configuratio
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Returns server-visible CPU, memory, and storage filesystem resource usage.
|
||||
* @summary Retrieve System Resources
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getSystemResources: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/v1/system/resources`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication SessionCookie required
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Deletes completed runs matching the provided filters, or previews the deletion set when dry-run is enabled.
|
||||
* @summary Prune Runs
|
||||
|
|
@ -292,6 +330,18 @@ export const SystemApiFp = function(configuration?: Configuration) {
|
|||
const localVarOperationServerBasePath = operationServerMap['SystemApi.getSystemRepairRuns']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Returns server-visible CPU, memory, and storage filesystem resource usage.
|
||||
* @summary Retrieve System Resources
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getSystemResources(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SystemResourcesResponse>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getSystemResources(options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['SystemApi.getSystemResources']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Deletes completed runs matching the provided filters, or previews the deletion set when dry-run is enabled.
|
||||
* @summary Prune Runs
|
||||
|
|
@ -352,6 +402,15 @@ export const SystemApiFactory = function (configuration?: Configuration, basePat
|
|||
getSystemRepairRuns(options?: RawAxiosRequestConfig): AxiosPromise<SystemRepairRunsResponse> {
|
||||
return localVarFp.getSystemRepairRuns(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Returns server-visible CPU, memory, and storage filesystem resource usage.
|
||||
* @summary Retrieve System Resources
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getSystemResources(options?: RawAxiosRequestConfig): AxiosPromise<SystemResourcesResponse> {
|
||||
return localVarFp.getSystemResources(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Deletes completed runs matching the provided filters, or previews the deletion set when dry-run is enabled.
|
||||
* @summary Prune Runs
|
||||
|
|
@ -411,6 +470,16 @@ export class SystemApi extends BaseAPI {
|
|||
return SystemApiFp(this.configuration).getSystemRepairRuns(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns server-visible CPU, memory, and storage filesystem resource usage.
|
||||
* @summary Retrieve System Resources
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public getSystemResources(options?: RawAxiosRequestConfig) {
|
||||
return SystemApiFp(this.configuration).getSystemResources(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes completed runs matching the provided filters, or previews the deletion set when dry-run is enabled.
|
||||
* @summary Prune Runs
|
||||
|
|
|
|||
|
|
@ -354,10 +354,17 @@ export * from './submit-answer-yes-request';
|
|||
export * from './submit-turn-request';
|
||||
export * from './success-reason';
|
||||
export * from './system-actor-kind';
|
||||
export * from './system-cpu-resource-scope';
|
||||
export * from './system-cpu-resources';
|
||||
export * from './system-disk-resource-scope';
|
||||
export * from './system-disk-resources';
|
||||
export * from './system-features';
|
||||
export * from './system-info-response';
|
||||
export * from './system-memory-resource-scope';
|
||||
export * from './system-memory-resources';
|
||||
export * from './system-repair-run-issue';
|
||||
export * from './system-repair-runs-response';
|
||||
export * from './system-resources-response';
|
||||
export * from './system-run-counts';
|
||||
export * from './timeline-entry-response';
|
||||
export * from './tls-mode';
|
||||
|
|
|
|||
25
lib/packages/fabro-api-client/src/models/system-cpu-resource-scope.ts
generated
Normal file
25
lib/packages/fabro-api-client/src/models/system-cpu-resource-scope.ts
generated
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Scope for CPU resource metrics.
|
||||
*/
|
||||
|
||||
export const SystemCpuResourceScope = {
|
||||
SERVER_ENVIRONMENT: 'server_environment'
|
||||
} as const;
|
||||
|
||||
export type SystemCpuResourceScope = typeof SystemCpuResourceScope[keyof typeof SystemCpuResourceScope];
|
||||
46
lib/packages/fabro-api-client/src/models/system-cpu-resources.ts
generated
Normal file
46
lib/packages/fabro-api-client/src/models/system-cpu-resources.ts
generated
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SystemCpuResourceScope } from './system-cpu-resource-scope';
|
||||
|
||||
/**
|
||||
* CPU resources visible to the Fabro server process.
|
||||
*/
|
||||
export interface SystemCpuResources {
|
||||
/**
|
||||
* Whether CPU metrics are available on this platform.
|
||||
*/
|
||||
'supported': boolean;
|
||||
'scope': SystemCpuResourceScope;
|
||||
/**
|
||||
* Reason metrics are unavailable when unsupported.
|
||||
*/
|
||||
'unavailable_reason': string | null;
|
||||
/**
|
||||
* Logical CPUs visible to the server process.
|
||||
*/
|
||||
'logical_cpus': number | null;
|
||||
/**
|
||||
* CPU usage percentage, null until a delta sample is available.
|
||||
*/
|
||||
'usage_percent': number | null;
|
||||
/**
|
||||
* Elapsed milliseconds since the previous CPU sample, null until a delta sample is available.
|
||||
*/
|
||||
'sample_window_ms': number | null;
|
||||
}
|
||||
|
||||
25
lib/packages/fabro-api-client/src/models/system-disk-resource-scope.ts
generated
Normal file
25
lib/packages/fabro-api-client/src/models/system-disk-resource-scope.ts
generated
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Scope for disk resource metrics.
|
||||
*/
|
||||
|
||||
export const SystemDiskResourceScope = {
|
||||
STORAGE_FILESYSTEM: 'storage_filesystem'
|
||||
} as const;
|
||||
|
||||
export type SystemDiskResourceScope = typeof SystemDiskResourceScope[keyof typeof SystemDiskResourceScope];
|
||||
70
lib/packages/fabro-api-client/src/models/system-disk-resources.ts
generated
Normal file
70
lib/packages/fabro-api-client/src/models/system-disk-resources.ts
generated
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SystemDiskResourceScope } from './system-disk-resource-scope';
|
||||
|
||||
/**
|
||||
* Filesystem resources for the configured Fabro storage root.
|
||||
*/
|
||||
export interface SystemDiskResources {
|
||||
/**
|
||||
* Whether storage filesystem metrics are available.
|
||||
*/
|
||||
'supported': boolean;
|
||||
'scope': SystemDiskResourceScope;
|
||||
/**
|
||||
* Reason metrics are unavailable when unsupported.
|
||||
*/
|
||||
'unavailable_reason': string | null;
|
||||
/**
|
||||
* Configured Fabro storage directory.
|
||||
*/
|
||||
'storage_path': string;
|
||||
/**
|
||||
* Mount point for the filesystem containing the storage directory.
|
||||
*/
|
||||
'mount_point': string | null;
|
||||
/**
|
||||
* Filesystem name reported by the operating system.
|
||||
*/
|
||||
'filesystem': string | null;
|
||||
/**
|
||||
* Total bytes on the storage filesystem.
|
||||
*/
|
||||
'total_bytes': number | null;
|
||||
/**
|
||||
* Used bytes on the storage filesystem.
|
||||
*/
|
||||
'used_bytes': number | null;
|
||||
/**
|
||||
* Available bytes on the storage filesystem.
|
||||
*/
|
||||
'available_bytes': number | null;
|
||||
/**
|
||||
* Used percentage on the storage filesystem.
|
||||
*/
|
||||
'used_percent': number | null;
|
||||
/**
|
||||
* Bytes managed by Fabro under the storage root.
|
||||
*/
|
||||
'fabro_managed_bytes': number;
|
||||
/**
|
||||
* Bytes reclaimable through Fabro pruning.
|
||||
*/
|
||||
'fabro_reclaimable_bytes': number;
|
||||
}
|
||||
|
||||
26
lib/packages/fabro-api-client/src/models/system-memory-resource-scope.ts
generated
Normal file
26
lib/packages/fabro-api-client/src/models/system-memory-resource-scope.ts
generated
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* `cgroup` when Linux cgroup limits are available, otherwise `host`.
|
||||
*/
|
||||
|
||||
export const SystemMemoryResourceScope = {
|
||||
HOST: 'host',
|
||||
CGROUP: 'cgroup'
|
||||
} as const;
|
||||
|
||||
export type SystemMemoryResourceScope = typeof SystemMemoryResourceScope[keyof typeof SystemMemoryResourceScope];
|
||||
54
lib/packages/fabro-api-client/src/models/system-memory-resources.ts
generated
Normal file
54
lib/packages/fabro-api-client/src/models/system-memory-resources.ts
generated
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SystemMemoryResourceScope } from './system-memory-resource-scope';
|
||||
|
||||
/**
|
||||
* Memory resources visible to the Fabro server process.
|
||||
*/
|
||||
export interface SystemMemoryResources {
|
||||
/**
|
||||
* Whether memory metrics are available on this platform.
|
||||
*/
|
||||
'supported': boolean;
|
||||
'scope': SystemMemoryResourceScope;
|
||||
/**
|
||||
* Reason metrics are unavailable when unsupported.
|
||||
*/
|
||||
'unavailable_reason': string | null;
|
||||
/**
|
||||
* Total memory for the reported scope.
|
||||
*/
|
||||
'total_bytes': number | null;
|
||||
/**
|
||||
* Used memory for the reported scope.
|
||||
*/
|
||||
'used_bytes': number | null;
|
||||
/**
|
||||
* Available memory for the reported scope.
|
||||
*/
|
||||
'available_bytes': number | null;
|
||||
/**
|
||||
* Used memory percentage for the reported scope.
|
||||
*/
|
||||
'used_percent': number | null;
|
||||
/**
|
||||
* Host total memory, included even when reporting a cgroup scope.
|
||||
*/
|
||||
'host_total_bytes': number | null;
|
||||
}
|
||||
|
||||
42
lib/packages/fabro-api-client/src/models/system-resources-response.ts
generated
Normal file
42
lib/packages/fabro-api-client/src/models/system-resources-response.ts
generated
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SystemCpuResources } from './system-cpu-resources';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SystemDiskResources } from './system-disk-resources';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SystemMemoryResources } from './system-memory-resources';
|
||||
|
||||
/**
|
||||
* Server-visible runtime resource usage for the active Fabro process environment.
|
||||
*/
|
||||
export interface SystemResourcesResponse {
|
||||
/**
|
||||
* Timestamp when the sample was collected.
|
||||
*/
|
||||
'sampled_at': string;
|
||||
'cpu': SystemCpuResources;
|
||||
'memory': SystemMemoryResources;
|
||||
'disk': SystemDiskResources;
|
||||
/**
|
||||
* Human-readable caveats about unavailable or scoped metrics.
|
||||
*/
|
||||
'notes': Array<string>;
|
||||
}
|
||||
|
||||
Loading…
Add table
Reference in a new issue