feat: add more disk I/O metrics (#1866)

Co-authored-by: henrygd <hank@henrygd.me>
This commit is contained in:
Sven van Ginkel
2026-04-05 00:28:05 +02:00
committed by GitHub
parent ef0c1420d1
commit c4009f2b43
12 changed files with 767 additions and 356 deletions

View File

@@ -34,6 +34,34 @@ type diskDiscovery struct {
ctx fsRegistrationContext
}
// prevDisk stores previous per-device disk counters for a given cache interval
type prevDisk struct {
readBytes uint64
writeBytes uint64
readTime uint64 // cumulative ms spent on reads (from ReadTime)
writeTime uint64 // cumulative ms spent on writes (from WriteTime)
ioTime uint64 // cumulative ms spent doing I/O (from IoTime)
weightedIO uint64 // cumulative weighted ms (queue-depth × ms, from WeightedIO)
readCount uint64 // cumulative read operation count
writeCount uint64 // cumulative write operation count
at time.Time
}
// prevDiskFromCounter creates a prevDisk snapshot from a disk.IOCountersStat at time t.
func prevDiskFromCounter(d disk.IOCountersStat, t time.Time) prevDisk {
return prevDisk{
readBytes: d.ReadBytes,
writeBytes: d.WriteBytes,
readTime: d.ReadTime,
writeTime: d.WriteTime,
ioTime: d.IoTime,
weightedIO: d.WeightedIO,
readCount: d.ReadCount,
writeCount: d.WriteCount,
at: t,
}
}
// parseFilesystemEntry parses a filesystem entry in the format "device__customname"
// Returns the device/filesystem part and the custom name part
func parseFilesystemEntry(entry string) (device, customName string) {
@@ -581,16 +609,29 @@ func (a *Agent) updateDiskIo(cacheTimeMs uint16, systemStats *system.Stats) {
prev, hasPrev := a.diskPrev[cacheTimeMs][name]
if !hasPrev {
// Seed from agent-level fsStats if present, else seed from current
prev = prevDisk{readBytes: stats.TotalRead, writeBytes: stats.TotalWrite, at: stats.Time}
prev = prevDisk{
readBytes: stats.TotalRead,
writeBytes: stats.TotalWrite,
readTime: d.ReadTime,
writeTime: d.WriteTime,
ioTime: d.IoTime,
weightedIO: d.WeightedIO,
readCount: d.ReadCount,
writeCount: d.WriteCount,
at: stats.Time,
}
if prev.at.IsZero() {
prev = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now}
prev = prevDiskFromCounter(d, now)
}
}
msElapsed := uint64(now.Sub(prev.at).Milliseconds())
// Update per-interval snapshot
a.diskPrev[cacheTimeMs][name] = prevDiskFromCounter(d, now)
// Avoid division by zero or clock issues
if msElapsed < 100 {
// Avoid division by zero or clock issues; update snapshot and continue
a.diskPrev[cacheTimeMs][name] = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now}
continue
}
@@ -602,15 +643,38 @@ func (a *Agent) updateDiskIo(cacheTimeMs uint16, systemStats *system.Stats) {
// validate values
if readMbPerSecond > 50_000 || writeMbPerSecond > 50_000 {
slog.Warn("Invalid disk I/O. Resetting.", "name", d.Name, "read", readMbPerSecond, "write", writeMbPerSecond)
// Reset interval snapshot and seed from current
a.diskPrev[cacheTimeMs][name] = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now}
// also refresh agent baseline to avoid future negatives
a.initializeDiskIoStats(ioCounters)
continue
}
// Update per-interval snapshot
a.diskPrev[cacheTimeMs][name] = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now}
// These properties are calculated differently on different platforms,
// but generally represent cumulative time spent doing reads/writes on the device.
// This can surpass 100% if there are multiple concurrent I/O operations.
// Linux kernel docs:
// This is the total number of milliseconds spent by all reads (as
// measured from __make_request() to end_that_request_last()).
// https://www.kernel.org/doc/Documentation/iostats.txt (fields 4, 8)
diskReadTime := utils.TwoDecimals(float64(d.ReadTime-prev.readTime) / float64(msElapsed) * 100)
diskWriteTime := utils.TwoDecimals(float64(d.WriteTime-prev.writeTime) / float64(msElapsed) * 100)
// I/O utilization %: fraction of wall time the device had any I/O in progress (0-100).
diskIoUtilPct := utils.TwoDecimals(float64(d.IoTime-prev.ioTime) / float64(msElapsed) * 100)
// Weighted I/O: queue-depth weighted I/O time, normalized to interval (can exceed 100%).
// Linux kernel field 11: incremented by iops_in_progress × ms_since_last_update.
// Used to display queue depth. Multipled by 100 to increase accuracy of digit truncation (divided by 100 in UI).
diskWeightedIO := utils.TwoDecimals(float64(d.WeightedIO-prev.weightedIO) / float64(msElapsed) * 100)
// r_await / w_await: average time per read/write operation in milliseconds.
// Equivalent to r_await and w_await in iostat.
var rAwait, wAwait float64
if deltaReadCount := d.ReadCount - prev.readCount; deltaReadCount > 0 {
rAwait = utils.TwoDecimals(float64(d.ReadTime-prev.readTime) / float64(deltaReadCount))
}
if deltaWriteCount := d.WriteCount - prev.writeCount; deltaWriteCount > 0 {
wAwait = utils.TwoDecimals(float64(d.WriteTime-prev.writeTime) / float64(deltaWriteCount))
}
// Update global fsStats baseline for cross-interval correctness
stats.Time = now
@@ -620,12 +684,24 @@ func (a *Agent) updateDiskIo(cacheTimeMs uint16, systemStats *system.Stats) {
stats.DiskWritePs = writeMbPerSecond
stats.DiskReadBytes = diskIORead
stats.DiskWriteBytes = diskIOWrite
stats.DiskIoStats[0] = diskReadTime
stats.DiskIoStats[1] = diskWriteTime
stats.DiskIoStats[2] = diskIoUtilPct
stats.DiskIoStats[3] = rAwait
stats.DiskIoStats[4] = wAwait
stats.DiskIoStats[5] = diskWeightedIO
if stats.Root {
systemStats.DiskReadPs = stats.DiskReadPs
systemStats.DiskWritePs = stats.DiskWritePs
systemStats.DiskIO[0] = diskIORead
systemStats.DiskIO[1] = diskIOWrite
systemStats.DiskIoStats[0] = diskReadTime
systemStats.DiskIoStats[1] = diskWriteTime
systemStats.DiskIoStats[2] = diskIoUtilPct
systemStats.DiskIoStats[3] = rAwait
systemStats.DiskIoStats[4] = wAwait
systemStats.DiskIoStats[5] = diskWeightedIO
}
}
}

View File

@@ -8,7 +8,6 @@ import (
"os"
"runtime"
"strings"
"time"
"github.com/henrygd/beszel"
"github.com/henrygd/beszel/agent/battery"
@@ -23,13 +22,6 @@ import (
"github.com/shirou/gopsutil/v4/mem"
)
// prevDisk stores previous per-device disk counters for a given cache interval
type prevDisk struct {
readBytes uint64
writeBytes uint64
at time.Time
}
// Sets initial / non-changing values about the host system
func (a *Agent) refreshSystemDetails() {
a.systemInfo.AgentVersion = beszel.Version

View File

@@ -48,6 +48,8 @@ type Stats struct {
MaxDiskIO [2]uint64 `json:"diom,omitzero" cbor:"-"` // [max read bytes, max write bytes]
CpuBreakdown []float64 `json:"cpub,omitempty" cbor:"33,keyasint,omitempty"` // [user, system, iowait, steal, idle]
CpuCoresUsage Uint8Slice `json:"cpus,omitempty" cbor:"34,keyasint,omitempty"` // per-core busy usage [CPU0..]
DiskIoStats [6]float64 `json:"dios,omitzero" cbor:"35,keyasint,omitzero"` // [read time %, write time %, io utilization %, r_await ms, w_await ms, weighted io %]
MaxDiskIoStats [6]float64 `json:"diosm,omitzero" cbor:"-"` // max values for DiskIoStats
}
// Uint8Slice wraps []uint8 to customize JSON encoding while keeping CBOR efficient.
@@ -93,10 +95,12 @@ type FsStats struct {
MaxDiskReadPS float64 `json:"rm,omitempty" cbor:"-"`
MaxDiskWritePS float64 `json:"wm,omitempty" cbor:"-"`
// TODO: remove DiskReadPs and DiskWritePs in future release in favor of DiskReadBytes and DiskWriteBytes
DiskReadBytes uint64 `json:"rb" cbor:"6,keyasint,omitempty"`
DiskWriteBytes uint64 `json:"wb" cbor:"7,keyasint,omitempty"`
MaxDiskReadBytes uint64 `json:"rbm,omitempty" cbor:"-"`
MaxDiskWriteBytes uint64 `json:"wbm,omitempty" cbor:"-"`
DiskReadBytes uint64 `json:"rb" cbor:"6,keyasint,omitempty"`
DiskWriteBytes uint64 `json:"wb" cbor:"7,keyasint,omitempty"`
MaxDiskReadBytes uint64 `json:"rbm,omitempty" cbor:"-"`
MaxDiskWriteBytes uint64 `json:"wbm,omitempty" cbor:"-"`
DiskIoStats [6]float64 `json:"dios,omitzero" cbor:"8,keyasint,omitzero"` // [read time %, write time %, io utilization %, r_await ms, w_await ms, weighted io %]
MaxDiskIoStats [6]float64 `json:"diosm,omitzero" cbor:"-"` // max values for DiskIoStats
}
type NetIoStats struct {

View File

@@ -230,6 +230,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.Bandwidth[1] += stats.Bandwidth[1]
sum.DiskIO[0] += stats.DiskIO[0]
sum.DiskIO[1] += stats.DiskIO[1]
for i := range stats.DiskIoStats {
sum.DiskIoStats[i] += stats.DiskIoStats[i]
}
batterySum += int(stats.Battery[0])
sum.Battery[1] = stats.Battery[1]
@@ -254,6 +257,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.MaxBandwidth[1] = max(sum.MaxBandwidth[1], stats.MaxBandwidth[1], stats.Bandwidth[1])
sum.MaxDiskIO[0] = max(sum.MaxDiskIO[0], stats.MaxDiskIO[0], stats.DiskIO[0])
sum.MaxDiskIO[1] = max(sum.MaxDiskIO[1], stats.MaxDiskIO[1], stats.DiskIO[1])
for i := range stats.DiskIoStats {
sum.MaxDiskIoStats[i] = max(sum.MaxDiskIoStats[i], stats.MaxDiskIoStats[i], stats.DiskIoStats[i])
}
// Accumulate network interfaces
if sum.NetworkInterfaces == nil {
@@ -299,6 +305,10 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
fs.DiskWriteBytes += value.DiskWriteBytes
fs.MaxDiskReadBytes = max(fs.MaxDiskReadBytes, value.MaxDiskReadBytes, value.DiskReadBytes)
fs.MaxDiskWriteBytes = max(fs.MaxDiskWriteBytes, value.MaxDiskWriteBytes, value.DiskWriteBytes)
for i := range value.DiskIoStats {
fs.DiskIoStats[i] += value.DiskIoStats[i]
fs.MaxDiskIoStats[i] = max(fs.MaxDiskIoStats[i], value.MaxDiskIoStats[i], value.DiskIoStats[i])
}
}
}
@@ -350,6 +360,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.DiskWritePs = twoDecimals(sum.DiskWritePs / count)
sum.DiskIO[0] = sum.DiskIO[0] / uint64(count)
sum.DiskIO[1] = sum.DiskIO[1] / uint64(count)
for i := range sum.DiskIoStats {
sum.DiskIoStats[i] = twoDecimals(sum.DiskIoStats[i] / count)
}
sum.NetworkSent = twoDecimals(sum.NetworkSent / count)
sum.NetworkRecv = twoDecimals(sum.NetworkRecv / count)
sum.LoadAvg[0] = twoDecimals(sum.LoadAvg[0] / count)
@@ -388,6 +401,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
fs.DiskReadPs = twoDecimals(fs.DiskReadPs / count)
fs.DiskReadBytes = fs.DiskReadBytes / uint64(count)
fs.DiskWriteBytes = fs.DiskWriteBytes / uint64(count)
for i := range fs.DiskIoStats {
fs.DiskIoStats[i] = twoDecimals(fs.DiskIoStats[i] / count)
}
}
}

View File

@@ -41,8 +41,8 @@ export default function AreaChartDefault({
hideYAxis = false,
filter,
truncate = false,
}: // logRender = false,
{
chartProps,
}: {
chartData: ChartData
// biome-ignore lint/suspicious/noExplicitAny: accepts different data source types (systemStats or containerData)
customData?: any[]
@@ -62,7 +62,7 @@ export default function AreaChartDefault({
hideYAxis?: boolean
filter?: string
truncate?: boolean
// logRender?: boolean
chartProps?: Omit<React.ComponentProps<typeof AreaChart>, "data" | "margin">
}) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
@@ -131,6 +131,7 @@ export default function AreaChartDefault({
accessibilityLayer
data={displayData}
margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}
{...chartProps}
>
<CartesianGrid vertical={false} />
{!hideYAxis && (

View File

@@ -40,8 +40,8 @@ export default function LineChartDefault({
hideYAxis = false,
filter,
truncate = false,
}: // logRender = false,
{
chartProps,
}: {
chartData: ChartData
// biome-ignore lint/suspicious/noExplicitAny: accepts different data source types (systemStats or containerData)
customData?: any[]
@@ -61,7 +61,7 @@ export default function LineChartDefault({
hideYAxis?: boolean
filter?: string
truncate?: boolean
// logRender?: boolean
chartProps?: Omit<React.ComponentProps<typeof LineChart>, "data" | "margin">
}) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
@@ -130,6 +130,7 @@ export default function LineChartDefault({
accessibilityLayer
data={displayData}
margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}
{...chartProps}
>
<CartesianGrid vertical={false} />
{!hideYAxis && (

View File

@@ -1,18 +1,16 @@
import { memo, useState } from "react"
import { Trans } from "@lingui/react/macro"
import { compareSemVer, parseSemVer } from "@/lib/utils"
import type { GPUData } from "@/types"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import InfoBar from "./system/info-bar"
import { useSystemData } from "./system/use-system-data"
import { CpuChart, ContainerCpuChart } from "./system/charts/cpu-charts"
import { MemoryChart, ContainerMemoryChart, SwapChart } from "./system/charts/memory-charts"
import { DiskCharts } from "./system/charts/disk-charts"
import { RootDiskCharts, ExtraFsCharts } from "./system/charts/disk-charts"
import { BandwidthChart, ContainerNetworkChart } from "./system/charts/network-charts"
import { TemperatureChart, BatteryChart } from "./system/charts/sensor-charts"
import { GpuPowerChart, GpuDetailCharts } from "./system/charts/gpu-charts"
import { ExtraFsCharts } from "./system/charts/extra-fs-charts"
import { LazyContainersTable, LazySmartTable, LazySystemdTable } from "./system/lazy-tables"
import { LoadAverageChart } from "./system/charts/load-average-chart"
import { ContainerIcon, CpuIcon, HardDriveIcon, TerminalSquareIcon } from "lucide-react"
@@ -24,6 +22,8 @@ const SEMVER_0_14_0 = parseSemVer("0.14.0")
const SEMVER_0_15_0 = parseSemVer("0.15.0")
export default memo(function SystemDetail({ id }: { id: string }) {
const systemData = useSystemData(id)
const {
system,
systemStats,
@@ -48,7 +48,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
hasGpuData,
hasGpuEnginesData,
hasGpuPowerData,
} = useSystemData(id)
} = systemData
// extra margin to add to bottom of page, specifically for temperature chart,
// where the tooltip can go past the bottom of the page if lots of sensors
@@ -103,7 +103,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
/>
)}
<DiskCharts {...coreProps} systemStats={systemStats} />
<RootDiskCharts systemData={systemData} />
<BandwidthChart {...coreProps} systemStats={systemStats} />
@@ -138,7 +138,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
/>
)}
<ExtraFsCharts {...coreProps} systemStats={systemStats} />
<ExtraFsCharts systemData={systemData} />
{maybeHasSmartData && <LazySmartTable systemId={system.id} />}
@@ -198,9 +198,9 @@ export default memo(function SystemDetail({ id }: { id: string }) {
{mountedTabs.has("disk") && (
<>
<div className="grid xl:grid-cols-2 gap-4">
<DiskCharts {...coreProps} systemStats={systemStats} />
<RootDiskCharts systemData={systemData} />
</div>
<ExtraFsCharts {...coreProps} systemStats={systemStats} />
<ExtraFsCharts systemData={systemData} />
{maybeHasSmartData && <LazySmartTable systemId={system.id} />}
</>
)}

View File

@@ -1,106 +1,280 @@
import { t } from "@lingui/core/macro"
import AreaChartDefault from "@/components/charts/area-chart"
import { $userSettings } from "@/lib/stores"
import { decimalString, formatBytes, toFixedFloat } from "@/lib/utils"
import type { ChartData, SystemStatsRecord } from "@/types"
import type { SystemStatsRecord } from "@/types"
import { ChartCard, SelectAvgMax } from "../chart-card"
import { Unit } from "@/lib/enums"
import { pinnedAxisDomain } from "@/components/ui/chart"
import DiskIoSheet from "../disk-io-sheet"
import type { SystemData } from "../use-system-data"
import { useStore } from "@nanostores/react"
import { $userSettings } from "@/lib/stores"
export function DiskCharts({
chartData,
grid,
dataEmpty,
showMax,
isLongerChart,
maxValues,
}: {
chartData: ChartData
grid: boolean
dataEmpty: boolean
showMax: boolean
isLongerChart: boolean
maxValues: boolean
systemStats: SystemStatsRecord[]
}) {
const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null
const userSettings = $userSettings.get()
// Helpers for indexed dios/diosm access
const dios =
(i: number) =>
({ stats }: SystemStatsRecord) =>
stats?.dios?.[i] ?? 0
const diosMax =
(i: number) =>
({ stats }: SystemStatsRecord) =>
stats?.diosm?.[i] ?? 0
const extraDios =
(name: string, i: number) =>
({ stats }: SystemStatsRecord) =>
stats?.efs?.[name]?.dios?.[i] ?? 0
const extraDiosMax =
(name: string, i: number) =>
({ stats }: SystemStatsRecord) =>
stats?.efs?.[name]?.diosm?.[i] ?? 0
export const diskDataFns = {
// usage
usage: ({ stats }: SystemStatsRecord) => stats?.du ?? 0,
extraUsage:
(name: string) =>
({ stats }: SystemStatsRecord) =>
stats?.efs?.[name]?.du ?? 0,
// throughput
read: ({ stats }: SystemStatsRecord) => stats?.dio?.[0] ?? (stats?.dr ?? 0) * 1024 * 1024,
readMax: ({ stats }: SystemStatsRecord) => stats?.diom?.[0] ?? (stats?.drm ?? 0) * 1024 * 1024,
write: ({ stats }: SystemStatsRecord) => stats?.dio?.[1] ?? (stats?.dw ?? 0) * 1024 * 1024,
writeMax: ({ stats }: SystemStatsRecord) => stats?.diom?.[1] ?? (stats?.dwm ?? 0) * 1024 * 1024,
// extra fs throughput
extraRead:
(name: string) =>
({ stats }: SystemStatsRecord) =>
stats?.efs?.[name]?.rb ?? (stats?.efs?.[name]?.r ?? 0) * 1024 * 1024,
extraReadMax:
(name: string) =>
({ stats }: SystemStatsRecord) =>
stats?.efs?.[name]?.rbm ?? (stats?.efs?.[name]?.rm ?? 0) * 1024 * 1024,
extraWrite:
(name: string) =>
({ stats }: SystemStatsRecord) =>
stats?.efs?.[name]?.wb ?? (stats?.efs?.[name]?.w ?? 0) * 1024 * 1024,
extraWriteMax:
(name: string) =>
({ stats }: SystemStatsRecord) =>
stats?.efs?.[name]?.wbm ?? (stats?.efs?.[name]?.wm ?? 0) * 1024 * 1024,
// read/write time
readTime: dios(0),
readTimeMax: diosMax(0),
extraReadTime: (name: string) => extraDios(name, 0),
extraReadTimeMax: (name: string) => extraDiosMax(name, 0),
writeTime: dios(1),
writeTimeMax: diosMax(1),
extraWriteTime: (name: string) => extraDios(name, 1),
extraWriteTimeMax: (name: string) => extraDiosMax(name, 1),
// utilization (IoTime-based, 0-100%)
util: dios(2),
utilMax: diosMax(2),
extraUtil: (name: string) => extraDios(name, 2),
extraUtilMax: (name: string) => extraDiosMax(name, 2),
// r_await / w_await: average service time per read/write operation (ms)
rAwait: dios(3),
rAwaitMax: diosMax(3),
extraRAwait: (name: string) => extraDios(name, 3),
extraRAwaitMax: (name: string) => extraDiosMax(name, 3),
wAwait: dios(4),
wAwaitMax: diosMax(4),
extraWAwait: (name: string) => extraDios(name, 4),
extraWAwaitMax: (name: string) => extraDiosMax(name, 4),
// average queue depth: stored as queue_depth * 100 in Go, divided here
weightedIO: ({ stats }: SystemStatsRecord) => (stats?.dios?.[5] ?? 0) / 100,
weightedIOMax: ({ stats }: SystemStatsRecord) => (stats?.diosm?.[5] ?? 0) / 100,
extraWeightedIO:
(name: string) =>
({ stats }: SystemStatsRecord) =>
(stats?.efs?.[name]?.dios?.[5] ?? 0) / 100,
extraWeightedIOMax:
(name: string) =>
({ stats }: SystemStatsRecord) =>
(stats?.efs?.[name]?.diosm?.[5] ?? 0) / 100,
}
export function RootDiskCharts({ systemData }: { systemData: SystemData }) {
return (
<>
<DiskUsageChart systemData={systemData} />
<DiskIOChart systemData={systemData} />
</>
)
}
export function DiskUsageChart({ systemData, extraFsName }: { systemData: SystemData; extraFsName?: string }) {
const { chartData, grid, dataEmpty } = systemData
let diskSize = chartData.systemStats?.at(-1)?.stats.d ?? NaN
if (extraFsName) {
diskSize = chartData.systemStats?.at(-1)?.stats.efs?.[extraFsName]?.d ?? NaN
}
// round to nearest GB
if (diskSize >= 100) {
diskSize = Math.round(diskSize)
}
return (
<>
<ChartCard empty={dataEmpty} grid={grid} title={t`Disk Usage`} description={t`Usage of root partition`}>
<AreaChartDefault
chartData={chartData}
domain={[0, diskSize]}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val * 1024, false, Unit.Bytes, true)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
return `${decimalString(convertedValue)} ${unit}`
}}
dataPoints={[
{
label: t`Disk Usage`,
color: 4,
opacity: 0.4,
dataKey: ({ stats }) => stats?.du,
},
]}
></AreaChartDefault>
</ChartCard>
const title = extraFsName ? `${extraFsName} ${t`Usage`}` : t`Disk Usage`
const description = extraFsName ? t`Disk usage of ${extraFsName}` : t`Usage of root partition`
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Disk I/O`}
description={t`Throughput of root filesystem`}
cornerEl={maxValSelect}
>
<AreaChartDefault
chartData={chartData}
maxToggled={showMax}
dataPoints={[
{
label: t({ message: "Write", comment: "Disk write" }),
dataKey: ({ stats }: SystemStatsRecord) => {
if (showMax) {
return stats?.dio?.[1] ?? (stats?.dwm ?? 0) * 1024 * 1024
}
return stats?.dio?.[1] ?? (stats?.dw ?? 0) * 1024 * 1024
},
color: 3,
opacity: 0.3,
},
{
label: t({ message: "Read", comment: "Disk read" }),
dataKey: ({ stats }: SystemStatsRecord) => {
if (showMax) {
return stats?.diom?.[0] ?? (stats?.drm ?? 0) * 1024 * 1024
}
return stats?.dio?.[0] ?? (stats?.dr ?? 0) * 1024 * 1024
},
color: 1,
opacity: 0.3,
},
]}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, true, userSettings.unitDisk, false)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, false)
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
}}
showTotal={true}
/>
</ChartCard>
</>
return (
<ChartCard empty={dataEmpty} grid={grid} title={title} description={description}>
<AreaChartDefault
chartData={chartData}
domain={[0, diskSize]}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val * 1024, false, Unit.Bytes, true)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
return `${decimalString(convertedValue)} ${unit}`
}}
dataPoints={[
{
label: t`Disk Usage`,
color: 4,
opacity: 0.4,
dataKey: extraFsName ? diskDataFns.extraUsage(extraFsName) : diskDataFns.usage,
},
]}
></AreaChartDefault>
</ChartCard>
)
}
export function DiskIOChart({ systemData, extraFsName }: { systemData: SystemData; extraFsName?: string }) {
const { chartData, grid, dataEmpty, showMax, isLongerChart, maxValues } = systemData
const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null
const userSettings = useStore($userSettings)
if (!chartData.systemStats?.length) {
return null
}
const title = extraFsName ? t`${extraFsName} I/O` : t`Disk I/O`
const description = extraFsName ? t`Throughput of ${extraFsName}` : t`Throughput of root filesystem`
const hasMoreIOMetrics = chartData.systemStats?.some((record) => record.stats?.dios?.at(0))
let CornerEl = maxValSelect
if (hasMoreIOMetrics) {
CornerEl = (
<div className="flex gap-2">
{maxValSelect}
<DiskIoSheet systemData={systemData} extraFsName={extraFsName} title={title} description={description} />
</div>
)
}
let readFn = showMax ? diskDataFns.readMax : diskDataFns.read
let writeFn = showMax ? diskDataFns.writeMax : diskDataFns.write
if (extraFsName) {
readFn = showMax ? diskDataFns.extraReadMax(extraFsName) : diskDataFns.extraRead(extraFsName)
writeFn = showMax ? diskDataFns.extraWriteMax(extraFsName) : diskDataFns.extraWrite(extraFsName)
}
return (
<ChartCard empty={dataEmpty} grid={grid} title={title} description={description} cornerEl={CornerEl}>
<AreaChartDefault
chartData={chartData}
maxToggled={showMax}
// domain={pinnedAxisDomain(true)}
showTotal={true}
dataPoints={[
{
label: t({ message: "Write", comment: "Disk write" }),
dataKey: writeFn,
color: 3,
opacity: 0.3,
},
{
label: t({ message: "Read", comment: "Disk read" }),
dataKey: readFn,
color: 1,
opacity: 0.3,
},
]}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, true, userSettings.unitDisk, false)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, false)
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
}}
/>
</ChartCard>
)
}
export function DiskUtilizationChart({ systemData, extraFsName }: { systemData: SystemData; extraFsName?: string }) {
const { chartData, grid, dataEmpty, showMax, isLongerChart, maxValues } = systemData
const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null
if (!chartData.systemStats?.length) {
return null
}
let utilFn = showMax ? diskDataFns.utilMax : diskDataFns.util
if (extraFsName) {
utilFn = showMax ? diskDataFns.extraUtilMax(extraFsName) : diskDataFns.extraUtil(extraFsName)
}
return (
<ChartCard
cornerEl={maxValSelect}
empty={dataEmpty}
grid={grid}
title={t`I/O Utilization`}
description={t`Percent of time the disk is busy with I/O`}
// legend={true}
className="min-h-auto"
>
<AreaChartDefault
chartData={chartData}
domain={pinnedAxisDomain()}
tickFormatter={(val) => `${toFixedFloat(val, 2)}%`}
contentFormatter={({ value }) => `${decimalString(value)}%`}
maxToggled={showMax}
chartProps={{ syncId: "io" }}
dataPoints={[
{
label: t`Utilization`,
dataKey: utilFn,
color: 1,
opacity: 0.4,
},
]}
/>
</ChartCard>
)
}
export function ExtraFsCharts({ systemData }: { systemData: SystemData }) {
const { systemStats } = systemData.chartData
const extraFs = systemStats?.at(-1)?.stats.efs
if (!extraFs || Object.keys(extraFs).length === 0) {
return null
}
return (
<div className="grid xl:grid-cols-2 gap-4">
{Object.keys(extraFs).map((extraFsName) => {
let diskSize = systemStats.at(-1)?.stats.efs?.[extraFsName].d ?? NaN
// round to nearest GB
if (diskSize >= 100) {
diskSize = Math.round(diskSize)
}
return (
<div key={extraFsName} className="contents">
<DiskUsageChart systemData={systemData} extraFsName={extraFsName} />
<DiskIOChart systemData={systemData} extraFsName={extraFsName} />
</div>
)
})}
</div>
)
}

View File

@@ -1,120 +0,0 @@
import { t } from "@lingui/core/macro"
import AreaChartDefault from "@/components/charts/area-chart"
import { $userSettings } from "@/lib/stores"
import { decimalString, formatBytes, toFixedFloat } from "@/lib/utils"
import type { ChartData, SystemStatsRecord } from "@/types"
import { ChartCard, SelectAvgMax } from "../chart-card"
import { Unit } from "@/lib/enums"
export function ExtraFsCharts({
chartData,
grid,
dataEmpty,
showMax,
isLongerChart,
maxValues,
systemStats,
}: {
chartData: ChartData
grid: boolean
dataEmpty: boolean
showMax: boolean
isLongerChart: boolean
maxValues: boolean
systemStats: SystemStatsRecord[]
}) {
const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null
const userSettings = $userSettings.get()
const extraFs = systemStats.at(-1)?.stats.efs
if (!extraFs || Object.keys(extraFs).length === 0) {
return null
}
return (
<div className="grid xl:grid-cols-2 gap-4">
{Object.keys(extraFs).map((extraFsName) => {
let diskSize = systemStats.at(-1)?.stats.efs?.[extraFsName].d ?? NaN
// round to nearest GB
if (diskSize >= 100) {
diskSize = Math.round(diskSize)
}
return (
<div key={extraFsName} className="contents">
<ChartCard
empty={dataEmpty}
grid={grid}
title={`${extraFsName} ${t`Usage`}`}
description={t`Disk usage of ${extraFsName}`}
>
<AreaChartDefault
chartData={chartData}
domain={[0, diskSize]}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val * 1024, false, Unit.Bytes, true)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
return `${decimalString(convertedValue)} ${unit}`
}}
dataPoints={[
{
label: t`Disk Usage`,
color: 4,
opacity: 0.4,
dataKey: ({ stats }) => stats?.efs?.[extraFsName]?.du,
},
]}
></AreaChartDefault>
</ChartCard>
<ChartCard
empty={dataEmpty}
grid={grid}
title={`${extraFsName} I/O`}
description={t`Throughput of ${extraFsName}`}
cornerEl={maxValSelect}
>
<AreaChartDefault
chartData={chartData}
showTotal={true}
dataPoints={[
{
label: t`Write`,
dataKey: ({ stats }) => {
if (showMax) {
return stats?.efs?.[extraFsName]?.wbm || (stats?.efs?.[extraFsName]?.wm ?? 0) * 1024 * 1024
}
return stats?.efs?.[extraFsName]?.wb || (stats?.efs?.[extraFsName]?.w ?? 0) * 1024 * 1024
},
color: 3,
opacity: 0.3,
},
{
label: t`Read`,
dataKey: ({ stats }) => {
if (showMax) {
return stats?.efs?.[extraFsName]?.rbm ?? (stats?.efs?.[extraFsName]?.rm ?? 0) * 1024 * 1024
}
return stats?.efs?.[extraFsName]?.rb ?? (stats?.efs?.[extraFsName]?.r ?? 0) * 1024 * 1024
},
color: 1,
opacity: 0.3,
},
]}
maxToggled={showMax}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, true, userSettings.unitDisk, false)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, false)
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
}}
/>
</ChartCard>
</div>
)
})}
</div>
)
}

View File

@@ -0,0 +1,257 @@
import { t } from "@lingui/core/macro"
import { useStore } from "@nanostores/react"
import { MoreHorizontalIcon } from "lucide-react"
import { memo, useRef, useState } from "react"
import AreaChartDefault from "@/components/charts/area-chart"
import ChartTimeSelect from "@/components/charts/chart-time-select"
import { Button } from "@/components/ui/button"
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
import { DialogTitle } from "@/components/ui/dialog"
import { $userSettings } from "@/lib/stores"
import { decimalString, formatBytes, toFixedFloat } from "@/lib/utils"
import { ChartCard, SelectAvgMax } from "@/components/routes/system/chart-card"
import type { SystemData } from "@/components/routes/system/use-system-data"
import { diskDataFns, DiskUtilizationChart } from "./charts/disk-charts"
import { pinnedAxisDomain } from "@/components/ui/chart"
export default memo(function DiskIOSheet({
systemData,
extraFsName,
title,
description,
}: {
systemData: SystemData
extraFsName?: string
title: string
description: string
}) {
const { chartData, grid, dataEmpty, showMax, maxValues, isLongerChart } = systemData
const userSettings = useStore($userSettings)
const [sheetOpen, setSheetOpen] = useState(false)
const hasOpened = useRef(false)
if (sheetOpen && !hasOpened.current) {
hasOpened.current = true
}
// throughput functions, with extra fs variants if needed
let readFn = showMax ? diskDataFns.readMax : diskDataFns.read
let writeFn = showMax ? diskDataFns.writeMax : diskDataFns.write
if (extraFsName) {
readFn = showMax ? diskDataFns.extraReadMax(extraFsName) : diskDataFns.extraRead(extraFsName)
writeFn = showMax ? diskDataFns.extraWriteMax(extraFsName) : diskDataFns.extraWrite(extraFsName)
}
// read and write time functions, with extra fs variants if needed
let readTimeFn = showMax ? diskDataFns.readTimeMax : diskDataFns.readTime
let writeTimeFn = showMax ? diskDataFns.writeTimeMax : diskDataFns.writeTime
if (extraFsName) {
readTimeFn = showMax ? diskDataFns.extraReadTimeMax(extraFsName) : diskDataFns.extraReadTime(extraFsName)
writeTimeFn = showMax ? diskDataFns.extraWriteTimeMax(extraFsName) : diskDataFns.extraWriteTime(extraFsName)
}
// I/O await functions, with extra fs variants if needed
let rAwaitFn = showMax ? diskDataFns.rAwaitMax : diskDataFns.rAwait
let wAwaitFn = showMax ? diskDataFns.wAwaitMax : diskDataFns.wAwait
if (extraFsName) {
rAwaitFn = showMax ? diskDataFns.extraRAwaitMax(extraFsName) : diskDataFns.extraRAwait(extraFsName)
wAwaitFn = showMax ? diskDataFns.extraWAwaitMax(extraFsName) : diskDataFns.extraWAwait(extraFsName)
}
// weighted I/O function, with extra fs variant if needed
let weightedIOFn = showMax ? diskDataFns.weightedIOMax : diskDataFns.weightedIO
if (extraFsName) {
weightedIOFn = showMax ? diskDataFns.extraWeightedIOMax(extraFsName) : diskDataFns.extraWeightedIO(extraFsName)
}
// check for availability of I/O metrics
let hasUtilization = false
let hasAwait = false
let hasWeightedIO = false
for (const record of chartData.systemStats ?? []) {
const dios = record.stats?.dios
if ((dios?.at(2) ?? 0) > 0) hasUtilization = true
if ((dios?.at(3) ?? 0) > 0) hasAwait = true
if ((dios?.at(5) ?? 0) > 0) hasWeightedIO = true
if (hasUtilization && hasAwait && hasWeightedIO) {
break
}
}
const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null
const chartProps = { syncId: "io" }
return (
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
<DialogTitle className="sr-only">{title}</DialogTitle>
<SheetTrigger asChild>
<Button
title={t`View more`}
variant="outline"
size="icon"
className="shrink-0 max-sm:absolute max-sm:top-0 max-sm:end-0"
>
<MoreHorizontalIcon />
</Button>
</SheetTrigger>
{hasOpened.current && (
<SheetContent aria-describedby={undefined} className="overflow-auto w-200 !max-w-full p-4 sm:p-6">
<ChartTimeSelect className="w-[calc(100%-2em)] bg-card" agentVersion={chartData.agentVersion} />
<ChartCard
className="min-h-auto"
empty={dataEmpty}
grid={grid}
title={title}
description={description}
cornerEl={maxValSelect}
// legend={true}
>
<AreaChartDefault
chartData={chartData}
maxToggled={showMax}
chartProps={chartProps}
showTotal={true}
domain={pinnedAxisDomain()}
itemSorter={(a, b) => a.order - b.order}
reverseStackOrder={true}
dataPoints={[
{
label: t({ message: "Write", comment: "Disk write" }),
dataKey: writeFn,
color: 3,
opacity: 0.4,
stackId: 0,
order: 0,
},
{
label: t({ message: "Read", comment: "Disk read" }),
dataKey: readFn,
color: 1,
opacity: 0.4,
stackId: 0,
order: 1,
},
]}
tickFormatter={(val) => {
const { value, unit } = formatBytes(val, true, userSettings.unitDisk, false)
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
}}
contentFormatter={({ value }) => {
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, false)
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
}}
/>
</ChartCard>
{hasUtilization && <DiskUtilizationChart systemData={systemData} extraFsName={extraFsName} />}
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`I/O Time`}
description={t`Total time spent on read/write (can exceed 100%)`}
className="min-h-auto"
cornerEl={maxValSelect}
>
<AreaChartDefault
chartData={chartData}
domain={pinnedAxisDomain()}
tickFormatter={(val) => `${toFixedFloat(val, 2)}%`}
contentFormatter={({ value }) => `${decimalString(value)}%`}
maxToggled={showMax}
chartProps={chartProps}
showTotal={true}
itemSorter={(a, b) => a.order - b.order}
reverseStackOrder={true}
dataPoints={[
{
label: t`Write`,
dataKey: writeTimeFn,
color: 3,
opacity: 0.4,
stackId: 0,
order: 0,
},
{
label: t`Read`,
dataKey: readTimeFn,
color: 1,
opacity: 0.4,
stackId: 0,
order: 1,
},
]}
/>
</ChartCard>
{hasWeightedIO && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Average Queue Depth`}
description={t`Average number of I/O operations waiting to be serviced`}
className="min-h-auto"
cornerEl={maxValSelect}
>
<AreaChartDefault
chartData={chartData}
domain={pinnedAxisDomain()}
tickFormatter={(val) => `${toFixedFloat(val, 2)}`}
contentFormatter={({ value }) => decimalString(value, value < 10 ? 3 : 2)}
maxToggled={showMax}
chartProps={chartProps}
dataPoints={[
{
label: t`Queue Depth`,
dataKey: weightedIOFn,
color: 1,
opacity: 0.4,
},
]}
/>
</ChartCard>
)}
{hasAwait && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`I/O Await`}
description={t`Average service time per operation`}
className="min-h-auto"
cornerEl={maxValSelect}
// legend={true}
>
<AreaChartDefault
chartData={chartData}
domain={pinnedAxisDomain()}
tickFormatter={(val) => `${toFixedFloat(val, 2)} ms`}
contentFormatter={({ value }) => `${decimalString(value)} ms`}
maxToggled={showMax}
chartProps={chartProps}
dataPoints={[
{
label: t`Write`,
dataKey: wAwaitFn,
color: 3,
opacity: 0.3,
},
{
label: t`Read`,
dataKey: rAwaitFn,
color: 1,
opacity: 0.3,
},
]}
/>
</ChartCard>
)}
</SheetContent>
)}
</Sheet>
)
})

View File

@@ -28,6 +28,8 @@ import type {
import { $router, navigate } from "../../router"
import { appendData, cache, getStats, getTimeData, makeContainerData, makeContainerPoint } from "./chart-data"
export type SystemData = ReturnType<typeof useSystemData>
export function useSystemData(id: string) {
const direction = useStore($direction)
const systems = useStore($systems)

View File

@@ -124,6 +124,10 @@ export interface SystemStats {
dio?: [number, number]
/** max disk I/O bytes [read, write] */
diom?: [number, number]
/** disk io stats [read time factor, write time factor, io utilization %, r_await ms, w_await ms, weighted io %] */
dios?: [number, number, number, number, number, number]
/** max disk io stats */
diosm?: [number, number, number, number, number, number]
/** network sent (mb) */
ns: number
/** network received (mb) */
@@ -186,6 +190,10 @@ export interface ExtraFsStats {
rbm: number
/** max write per second (mb) */
wbm: number
/** disk io stats [read time factor, write time factor, io utilization %, r_await ms, w_await ms, weighted io %] */
dios?: [number, number, number, number, number, number]
/** max disk io stats */
diosm?: [number, number, number, number, number, number]
}
export interface ContainerStatsRecord extends RecordModel {
@@ -413,118 +421,118 @@ export interface SystemdRecord extends RecordModel {
}
export interface SystemdServiceDetails {
AccessSELinuxContext: string;
ActivationDetails: any[];
ActiveEnterTimestamp: number;
ActiveEnterTimestampMonotonic: number;
ActiveExitTimestamp: number;
ActiveExitTimestampMonotonic: number;
ActiveState: string;
After: string[];
AllowIsolate: boolean;
AssertResult: boolean;
AssertTimestamp: number;
AssertTimestampMonotonic: number;
Asserts: any[];
Before: string[];
BindsTo: any[];
BoundBy: any[];
CPUUsageNSec: number;
CanClean: any[];
CanFreeze: boolean;
CanIsolate: boolean;
CanLiveMount: boolean;
CanReload: boolean;
CanStart: boolean;
CanStop: boolean;
CollectMode: string;
ConditionResult: boolean;
ConditionTimestamp: number;
ConditionTimestampMonotonic: number;
Conditions: any[];
ConflictedBy: any[];
Conflicts: string[];
ConsistsOf: any[];
DebugInvocation: boolean;
DefaultDependencies: boolean;
Description: string;
Documentation: string[];
DropInPaths: any[];
ExecMainPID: number;
FailureAction: string;
FailureActionExitStatus: number;
Following: string;
FragmentPath: string;
FreezerState: string;
Id: string;
IgnoreOnIsolate: boolean;
InactiveEnterTimestamp: number;
InactiveEnterTimestampMonotonic: number;
InactiveExitTimestamp: number;
InactiveExitTimestampMonotonic: number;
InvocationID: string;
Job: Array<number | string>;
JobRunningTimeoutUSec: number;
JobTimeoutAction: string;
JobTimeoutRebootArgument: string;
JobTimeoutUSec: number;
JoinsNamespaceOf: any[];
LoadError: string[];
LoadState: string;
MainPID: number;
Markers: any[];
MemoryCurrent: number;
MemoryLimit: number;
MemoryPeak: number;
NRestarts: number;
Names: string[];
NeedDaemonReload: boolean;
OnFailure: any[];
OnFailureJobMode: string;
OnFailureOf: any[];
OnSuccess: any[];
OnSuccessJobMode: string;
OnSuccessOf: any[];
PartOf: any[];
Perpetual: boolean;
PropagatesReloadTo: any[];
PropagatesStopTo: any[];
RebootArgument: string;
Refs: any[];
RefuseManualStart: boolean;
RefuseManualStop: boolean;
ReloadPropagatedFrom: any[];
RequiredBy: any[];
Requires: string[];
RequiresMountsFor: any[];
Requisite: any[];
RequisiteOf: any[];
Result: string;
SliceOf: any[];
SourcePath: string;
StartLimitAction: string;
StartLimitBurst: number;
StartLimitIntervalUSec: number;
StateChangeTimestamp: number;
StateChangeTimestampMonotonic: number;
StopPropagatedFrom: any[];
StopWhenUnneeded: boolean;
SubState: string;
SuccessAction: string;
SuccessActionExitStatus: number;
SurviveFinalKillSignal: boolean;
TasksCurrent: number;
TasksMax: number;
Transient: boolean;
TriggeredBy: string[];
Triggers: any[];
UnitFilePreset: string;
UnitFileState: string;
UpheldBy: any[];
Upholds: any[];
WantedBy: any[];
Wants: string[];
WantsMountsFor: any[];
AccessSELinuxContext: string
ActivationDetails: any[]
ActiveEnterTimestamp: number
ActiveEnterTimestampMonotonic: number
ActiveExitTimestamp: number
ActiveExitTimestampMonotonic: number
ActiveState: string
After: string[]
AllowIsolate: boolean
AssertResult: boolean
AssertTimestamp: number
AssertTimestampMonotonic: number
Asserts: any[]
Before: string[]
BindsTo: any[]
BoundBy: any[]
CPUUsageNSec: number
CanClean: any[]
CanFreeze: boolean
CanIsolate: boolean
CanLiveMount: boolean
CanReload: boolean
CanStart: boolean
CanStop: boolean
CollectMode: string
ConditionResult: boolean
ConditionTimestamp: number
ConditionTimestampMonotonic: number
Conditions: any[]
ConflictedBy: any[]
Conflicts: string[]
ConsistsOf: any[]
DebugInvocation: boolean
DefaultDependencies: boolean
Description: string
Documentation: string[]
DropInPaths: any[]
ExecMainPID: number
FailureAction: string
FailureActionExitStatus: number
Following: string
FragmentPath: string
FreezerState: string
Id: string
IgnoreOnIsolate: boolean
InactiveEnterTimestamp: number
InactiveEnterTimestampMonotonic: number
InactiveExitTimestamp: number
InactiveExitTimestampMonotonic: number
InvocationID: string
Job: Array<number | string>
JobRunningTimeoutUSec: number
JobTimeoutAction: string
JobTimeoutRebootArgument: string
JobTimeoutUSec: number
JoinsNamespaceOf: any[]
LoadError: string[]
LoadState: string
MainPID: number
Markers: any[]
MemoryCurrent: number
MemoryLimit: number
MemoryPeak: number
NRestarts: number
Names: string[]
NeedDaemonReload: boolean
OnFailure: any[]
OnFailureJobMode: string
OnFailureOf: any[]
OnSuccess: any[]
OnSuccessJobMode: string
OnSuccessOf: any[]
PartOf: any[]
Perpetual: boolean
PropagatesReloadTo: any[]
PropagatesStopTo: any[]
RebootArgument: string
Refs: any[]
RefuseManualStart: boolean
RefuseManualStop: boolean
ReloadPropagatedFrom: any[]
RequiredBy: any[]
Requires: string[]
RequiresMountsFor: any[]
Requisite: any[]
RequisiteOf: any[]
Result: string
SliceOf: any[]
SourcePath: string
StartLimitAction: string
StartLimitBurst: number
StartLimitIntervalUSec: number
StateChangeTimestamp: number
StateChangeTimestampMonotonic: number
StopPropagatedFrom: any[]
StopWhenUnneeded: boolean
SubState: string
SuccessAction: string
SuccessActionExitStatus: number
SurviveFinalKillSignal: boolean
TasksCurrent: number
TasksMax: number
Transient: boolean
TriggeredBy: string[]
Triggers: any[]
UnitFilePreset: string
UnitFileState: string
UpheldBy: any[]
Upholds: any[]
WantedBy: any[]
Wants: string[]
WantsMountsFor: any[]
}
export interface BeszelInfo {