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

![Resources settings
page](https://raw.githubusercontent.com/fabro-sh/fabro/feature/system-resources-settings/docs/public/images/web/settings-resources.jpg)

## 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`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound%20Engineering-Codex-6f42c1)](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:
Bryan Helmkamp 2026-05-20 17:22:53 -04:00 • committed by GitHub
parent 5eb874b55c
commit 40ed64c1c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1608 additions and 8 deletions

1
Cargo.lock generated
View file

@ -2304,6 +2304,7 @@ dependencies = [
"serde_yaml",
"sha2",
"strum",
"sysinfo",
"tempfile",
"thiserror 2.0.18",
"tokio",

View file

@ -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`;

View file

@ -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),

View file

@ -35,6 +35,7 @@ export const queryKeys = {
},
system: {
info: () => ["system", "info"] as const,
resources: () => ["system", "resources"] as const,
attachUrl: () => "/api/v1/attach",
},
boards: {

View file

@ -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");
});
});

View file

@ -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),
],
}),

View file

@ -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 (

View 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.",
);
});
});

View 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)}%`;
}

View file

@ -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",

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View file

@ -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" }

View file

@ -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>>,

View file

@ -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,

View file

@ -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))

View file

@ -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>>,

View 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,
}
}
}

View file

@ -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));

View file

@ -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

View file

@ -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

View file

@ -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';

View 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];

View 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;
}

View 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];

View 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;
}

View 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];

View 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;
}

View 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>;
}