mirror of
https://github.com/henrygd/beszel.git
synced 2026-04-17 02:11:49 +02:00
feat: add more disk I/O metrics (#1866)
Co-authored-by: henrygd <hank@henrygd.me>
This commit is contained in:
@@ -34,6 +34,34 @@ type diskDiscovery struct {
|
|||||||
ctx fsRegistrationContext
|
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"
|
// parseFilesystemEntry parses a filesystem entry in the format "device__customname"
|
||||||
// Returns the device/filesystem part and the custom name part
|
// Returns the device/filesystem part and the custom name part
|
||||||
func parseFilesystemEntry(entry string) (device, customName string) {
|
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]
|
prev, hasPrev := a.diskPrev[cacheTimeMs][name]
|
||||||
if !hasPrev {
|
if !hasPrev {
|
||||||
// Seed from agent-level fsStats if present, else seed from current
|
// 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() {
|
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())
|
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 {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,15 +643,38 @@ func (a *Agent) updateDiskIo(cacheTimeMs uint16, systemStats *system.Stats) {
|
|||||||
// validate values
|
// validate values
|
||||||
if readMbPerSecond > 50_000 || writeMbPerSecond > 50_000 {
|
if readMbPerSecond > 50_000 || writeMbPerSecond > 50_000 {
|
||||||
slog.Warn("Invalid disk I/O. Resetting.", "name", d.Name, "read", readMbPerSecond, "write", writeMbPerSecond)
|
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
|
// also refresh agent baseline to avoid future negatives
|
||||||
a.initializeDiskIoStats(ioCounters)
|
a.initializeDiskIoStats(ioCounters)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update per-interval snapshot
|
// These properties are calculated differently on different platforms,
|
||||||
a.diskPrev[cacheTimeMs][name] = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now}
|
// 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
|
// Update global fsStats baseline for cross-interval correctness
|
||||||
stats.Time = now
|
stats.Time = now
|
||||||
@@ -620,12 +684,24 @@ func (a *Agent) updateDiskIo(cacheTimeMs uint16, systemStats *system.Stats) {
|
|||||||
stats.DiskWritePs = writeMbPerSecond
|
stats.DiskWritePs = writeMbPerSecond
|
||||||
stats.DiskReadBytes = diskIORead
|
stats.DiskReadBytes = diskIORead
|
||||||
stats.DiskWriteBytes = diskIOWrite
|
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 {
|
if stats.Root {
|
||||||
systemStats.DiskReadPs = stats.DiskReadPs
|
systemStats.DiskReadPs = stats.DiskReadPs
|
||||||
systemStats.DiskWritePs = stats.DiskWritePs
|
systemStats.DiskWritePs = stats.DiskWritePs
|
||||||
systemStats.DiskIO[0] = diskIORead
|
systemStats.DiskIO[0] = diskIORead
|
||||||
systemStats.DiskIO[1] = diskIOWrite
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/henrygd/beszel"
|
"github.com/henrygd/beszel"
|
||||||
"github.com/henrygd/beszel/agent/battery"
|
"github.com/henrygd/beszel/agent/battery"
|
||||||
@@ -23,13 +22,6 @@ import (
|
|||||||
"github.com/shirou/gopsutil/v4/mem"
|
"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
|
// Sets initial / non-changing values about the host system
|
||||||
func (a *Agent) refreshSystemDetails() {
|
func (a *Agent) refreshSystemDetails() {
|
||||||
a.systemInfo.AgentVersion = beszel.Version
|
a.systemInfo.AgentVersion = beszel.Version
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ type Stats struct {
|
|||||||
MaxDiskIO [2]uint64 `json:"diom,omitzero" cbor:"-"` // [max read bytes, max write bytes]
|
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]
|
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..]
|
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.
|
// Uint8Slice wraps []uint8 to customize JSON encoding while keeping CBOR efficient.
|
||||||
@@ -93,10 +95,12 @@ type FsStats struct {
|
|||||||
MaxDiskReadPS float64 `json:"rm,omitempty" cbor:"-"`
|
MaxDiskReadPS float64 `json:"rm,omitempty" cbor:"-"`
|
||||||
MaxDiskWritePS float64 `json:"wm,omitempty" cbor:"-"`
|
MaxDiskWritePS float64 `json:"wm,omitempty" cbor:"-"`
|
||||||
// TODO: remove DiskReadPs and DiskWritePs in future release in favor of DiskReadBytes and DiskWriteBytes
|
// TODO: remove DiskReadPs and DiskWritePs in future release in favor of DiskReadBytes and DiskWriteBytes
|
||||||
DiskReadBytes uint64 `json:"rb" cbor:"6,keyasint,omitempty"`
|
DiskReadBytes uint64 `json:"rb" cbor:"6,keyasint,omitempty"`
|
||||||
DiskWriteBytes uint64 `json:"wb" cbor:"7,keyasint,omitempty"`
|
DiskWriteBytes uint64 `json:"wb" cbor:"7,keyasint,omitempty"`
|
||||||
MaxDiskReadBytes uint64 `json:"rbm,omitempty" cbor:"-"`
|
MaxDiskReadBytes uint64 `json:"rbm,omitempty" cbor:"-"`
|
||||||
MaxDiskWriteBytes uint64 `json:"wbm,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 {
|
type NetIoStats struct {
|
||||||
|
|||||||
@@ -230,6 +230,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
|||||||
sum.Bandwidth[1] += stats.Bandwidth[1]
|
sum.Bandwidth[1] += stats.Bandwidth[1]
|
||||||
sum.DiskIO[0] += stats.DiskIO[0]
|
sum.DiskIO[0] += stats.DiskIO[0]
|
||||||
sum.DiskIO[1] += stats.DiskIO[1]
|
sum.DiskIO[1] += stats.DiskIO[1]
|
||||||
|
for i := range stats.DiskIoStats {
|
||||||
|
sum.DiskIoStats[i] += stats.DiskIoStats[i]
|
||||||
|
}
|
||||||
batterySum += int(stats.Battery[0])
|
batterySum += int(stats.Battery[0])
|
||||||
sum.Battery[1] = stats.Battery[1]
|
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.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[0] = max(sum.MaxDiskIO[0], stats.MaxDiskIO[0], stats.DiskIO[0])
|
||||||
sum.MaxDiskIO[1] = max(sum.MaxDiskIO[1], stats.MaxDiskIO[1], stats.DiskIO[1])
|
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
|
// Accumulate network interfaces
|
||||||
if sum.NetworkInterfaces == nil {
|
if sum.NetworkInterfaces == nil {
|
||||||
@@ -299,6 +305,10 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
|||||||
fs.DiskWriteBytes += value.DiskWriteBytes
|
fs.DiskWriteBytes += value.DiskWriteBytes
|
||||||
fs.MaxDiskReadBytes = max(fs.MaxDiskReadBytes, value.MaxDiskReadBytes, value.DiskReadBytes)
|
fs.MaxDiskReadBytes = max(fs.MaxDiskReadBytes, value.MaxDiskReadBytes, value.DiskReadBytes)
|
||||||
fs.MaxDiskWriteBytes = max(fs.MaxDiskWriteBytes, value.MaxDiskWriteBytes, value.DiskWriteBytes)
|
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.DiskWritePs = twoDecimals(sum.DiskWritePs / count)
|
||||||
sum.DiskIO[0] = sum.DiskIO[0] / uint64(count)
|
sum.DiskIO[0] = sum.DiskIO[0] / uint64(count)
|
||||||
sum.DiskIO[1] = sum.DiskIO[1] / 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.NetworkSent = twoDecimals(sum.NetworkSent / count)
|
||||||
sum.NetworkRecv = twoDecimals(sum.NetworkRecv / count)
|
sum.NetworkRecv = twoDecimals(sum.NetworkRecv / count)
|
||||||
sum.LoadAvg[0] = twoDecimals(sum.LoadAvg[0] / 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.DiskReadPs = twoDecimals(fs.DiskReadPs / count)
|
||||||
fs.DiskReadBytes = fs.DiskReadBytes / uint64(count)
|
fs.DiskReadBytes = fs.DiskReadBytes / uint64(count)
|
||||||
fs.DiskWriteBytes = fs.DiskWriteBytes / uint64(count)
|
fs.DiskWriteBytes = fs.DiskWriteBytes / uint64(count)
|
||||||
|
for i := range fs.DiskIoStats {
|
||||||
|
fs.DiskIoStats[i] = twoDecimals(fs.DiskIoStats[i] / count)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ export default function AreaChartDefault({
|
|||||||
hideYAxis = false,
|
hideYAxis = false,
|
||||||
filter,
|
filter,
|
||||||
truncate = false,
|
truncate = false,
|
||||||
}: // logRender = false,
|
chartProps,
|
||||||
{
|
}: {
|
||||||
chartData: ChartData
|
chartData: ChartData
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: accepts different data source types (systemStats or containerData)
|
// biome-ignore lint/suspicious/noExplicitAny: accepts different data source types (systemStats or containerData)
|
||||||
customData?: any[]
|
customData?: any[]
|
||||||
@@ -62,7 +62,7 @@ export default function AreaChartDefault({
|
|||||||
hideYAxis?: boolean
|
hideYAxis?: boolean
|
||||||
filter?: string
|
filter?: string
|
||||||
truncate?: boolean
|
truncate?: boolean
|
||||||
// logRender?: boolean
|
chartProps?: Omit<React.ComponentProps<typeof AreaChart>, "data" | "margin">
|
||||||
}) {
|
}) {
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
|
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
|
||||||
@@ -131,6 +131,7 @@ export default function AreaChartDefault({
|
|||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={displayData}
|
data={displayData}
|
||||||
margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}
|
margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}
|
||||||
|
{...chartProps}
|
||||||
>
|
>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
{!hideYAxis && (
|
{!hideYAxis && (
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ export default function LineChartDefault({
|
|||||||
hideYAxis = false,
|
hideYAxis = false,
|
||||||
filter,
|
filter,
|
||||||
truncate = false,
|
truncate = false,
|
||||||
}: // logRender = false,
|
chartProps,
|
||||||
{
|
}: {
|
||||||
chartData: ChartData
|
chartData: ChartData
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: accepts different data source types (systemStats or containerData)
|
// biome-ignore lint/suspicious/noExplicitAny: accepts different data source types (systemStats or containerData)
|
||||||
customData?: any[]
|
customData?: any[]
|
||||||
@@ -61,7 +61,7 @@ export default function LineChartDefault({
|
|||||||
hideYAxis?: boolean
|
hideYAxis?: boolean
|
||||||
filter?: string
|
filter?: string
|
||||||
truncate?: boolean
|
truncate?: boolean
|
||||||
// logRender?: boolean
|
chartProps?: Omit<React.ComponentProps<typeof LineChart>, "data" | "margin">
|
||||||
}) {
|
}) {
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
|
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
|
||||||
@@ -130,6 +130,7 @@ export default function LineChartDefault({
|
|||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={displayData}
|
data={displayData}
|
||||||
margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}
|
margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}
|
||||||
|
{...chartProps}
|
||||||
>
|
>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
{!hideYAxis && (
|
{!hideYAxis && (
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import { memo, useState } from "react"
|
import { memo, useState } from "react"
|
||||||
import { Trans } from "@lingui/react/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { compareSemVer, parseSemVer } from "@/lib/utils"
|
import { compareSemVer, parseSemVer } from "@/lib/utils"
|
||||||
|
|
||||||
import type { GPUData } from "@/types"
|
import type { GPUData } from "@/types"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
import InfoBar from "./system/info-bar"
|
import InfoBar from "./system/info-bar"
|
||||||
import { useSystemData } from "./system/use-system-data"
|
import { useSystemData } from "./system/use-system-data"
|
||||||
import { CpuChart, ContainerCpuChart } from "./system/charts/cpu-charts"
|
import { CpuChart, ContainerCpuChart } from "./system/charts/cpu-charts"
|
||||||
import { MemoryChart, ContainerMemoryChart, SwapChart } from "./system/charts/memory-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 { BandwidthChart, ContainerNetworkChart } from "./system/charts/network-charts"
|
||||||
import { TemperatureChart, BatteryChart } from "./system/charts/sensor-charts"
|
import { TemperatureChart, BatteryChart } from "./system/charts/sensor-charts"
|
||||||
import { GpuPowerChart, GpuDetailCharts } from "./system/charts/gpu-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 { LazyContainersTable, LazySmartTable, LazySystemdTable } from "./system/lazy-tables"
|
||||||
import { LoadAverageChart } from "./system/charts/load-average-chart"
|
import { LoadAverageChart } from "./system/charts/load-average-chart"
|
||||||
import { ContainerIcon, CpuIcon, HardDriveIcon, TerminalSquareIcon } from "lucide-react"
|
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")
|
const SEMVER_0_15_0 = parseSemVer("0.15.0")
|
||||||
|
|
||||||
export default memo(function SystemDetail({ id }: { id: string }) {
|
export default memo(function SystemDetail({ id }: { id: string }) {
|
||||||
|
const systemData = useSystemData(id)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
system,
|
system,
|
||||||
systemStats,
|
systemStats,
|
||||||
@@ -48,7 +48,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
hasGpuData,
|
hasGpuData,
|
||||||
hasGpuEnginesData,
|
hasGpuEnginesData,
|
||||||
hasGpuPowerData,
|
hasGpuPowerData,
|
||||||
} = useSystemData(id)
|
} = systemData
|
||||||
|
|
||||||
// extra margin to add to bottom of page, specifically for temperature chart,
|
// 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
|
// 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} />
|
<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} />}
|
{maybeHasSmartData && <LazySmartTable systemId={system.id} />}
|
||||||
|
|
||||||
@@ -198,9 +198,9 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
{mountedTabs.has("disk") && (
|
{mountedTabs.has("disk") && (
|
||||||
<>
|
<>
|
||||||
<div className="grid xl:grid-cols-2 gap-4">
|
<div className="grid xl:grid-cols-2 gap-4">
|
||||||
<DiskCharts {...coreProps} systemStats={systemStats} />
|
<RootDiskCharts systemData={systemData} />
|
||||||
</div>
|
</div>
|
||||||
<ExtraFsCharts {...coreProps} systemStats={systemStats} />
|
<ExtraFsCharts systemData={systemData} />
|
||||||
{maybeHasSmartData && <LazySmartTable systemId={system.id} />}
|
{maybeHasSmartData && <LazySmartTable systemId={system.id} />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,106 +1,280 @@
|
|||||||
import { t } from "@lingui/core/macro"
|
import { t } from "@lingui/core/macro"
|
||||||
import AreaChartDefault from "@/components/charts/area-chart"
|
import AreaChartDefault from "@/components/charts/area-chart"
|
||||||
import { $userSettings } from "@/lib/stores"
|
|
||||||
import { decimalString, formatBytes, toFixedFloat } from "@/lib/utils"
|
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 { ChartCard, SelectAvgMax } from "../chart-card"
|
||||||
import { Unit } from "@/lib/enums"
|
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({
|
// Helpers for indexed dios/diosm access
|
||||||
chartData,
|
const dios =
|
||||||
grid,
|
(i: number) =>
|
||||||
dataEmpty,
|
({ stats }: SystemStatsRecord) =>
|
||||||
showMax,
|
stats?.dios?.[i] ?? 0
|
||||||
isLongerChart,
|
const diosMax =
|
||||||
maxValues,
|
(i: number) =>
|
||||||
}: {
|
({ stats }: SystemStatsRecord) =>
|
||||||
chartData: ChartData
|
stats?.diosm?.[i] ?? 0
|
||||||
grid: boolean
|
const extraDios =
|
||||||
dataEmpty: boolean
|
(name: string, i: number) =>
|
||||||
showMax: boolean
|
({ stats }: SystemStatsRecord) =>
|
||||||
isLongerChart: boolean
|
stats?.efs?.[name]?.dios?.[i] ?? 0
|
||||||
maxValues: boolean
|
const extraDiosMax =
|
||||||
systemStats: SystemStatsRecord[]
|
(name: string, i: number) =>
|
||||||
}) {
|
({ stats }: SystemStatsRecord) =>
|
||||||
const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null
|
stats?.efs?.[name]?.diosm?.[i] ?? 0
|
||||||
const userSettings = $userSettings.get()
|
|
||||||
|
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
|
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
|
// round to nearest GB
|
||||||
if (diskSize >= 100) {
|
if (diskSize >= 100) {
|
||||||
diskSize = Math.round(diskSize)
|
diskSize = Math.round(diskSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 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>
|
|
||||||
|
|
||||||
<ChartCard
|
return (
|
||||||
empty={dataEmpty}
|
<ChartCard empty={dataEmpty} grid={grid} title={title} description={description}>
|
||||||
grid={grid}
|
<AreaChartDefault
|
||||||
title={t`Disk I/O`}
|
chartData={chartData}
|
||||||
description={t`Throughput of root filesystem`}
|
domain={[0, diskSize]}
|
||||||
cornerEl={maxValSelect}
|
tickFormatter={(val) => {
|
||||||
>
|
const { value, unit } = formatBytes(val * 1024, false, Unit.Bytes, true)
|
||||||
<AreaChartDefault
|
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
|
||||||
chartData={chartData}
|
}}
|
||||||
maxToggled={showMax}
|
contentFormatter={({ value }) => {
|
||||||
dataPoints={[
|
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
|
||||||
{
|
return `${decimalString(convertedValue)} ${unit}`
|
||||||
label: t({ message: "Write", comment: "Disk write" }),
|
}}
|
||||||
dataKey: ({ stats }: SystemStatsRecord) => {
|
dataPoints={[
|
||||||
if (showMax) {
|
{
|
||||||
return stats?.dio?.[1] ?? (stats?.dwm ?? 0) * 1024 * 1024
|
label: t`Disk Usage`,
|
||||||
}
|
color: 4,
|
||||||
return stats?.dio?.[1] ?? (stats?.dw ?? 0) * 1024 * 1024
|
opacity: 0.4,
|
||||||
},
|
dataKey: extraFsName ? diskDataFns.extraUsage(extraFsName) : diskDataFns.usage,
|
||||||
color: 3,
|
},
|
||||||
opacity: 0.3,
|
]}
|
||||||
},
|
></AreaChartDefault>
|
||||||
{
|
</ChartCard>
|
||||||
label: t({ message: "Read", comment: "Disk read" }),
|
)
|
||||||
dataKey: ({ stats }: SystemStatsRecord) => {
|
}
|
||||||
if (showMax) {
|
|
||||||
return stats?.diom?.[0] ?? (stats?.drm ?? 0) * 1024 * 1024
|
export function DiskIOChart({ systemData, extraFsName }: { systemData: SystemData; extraFsName?: string }) {
|
||||||
}
|
const { chartData, grid, dataEmpty, showMax, isLongerChart, maxValues } = systemData
|
||||||
return stats?.dio?.[0] ?? (stats?.dr ?? 0) * 1024 * 1024
|
const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null
|
||||||
},
|
const userSettings = useStore($userSettings)
|
||||||
color: 1,
|
|
||||||
opacity: 0.3,
|
if (!chartData.systemStats?.length) {
|
||||||
},
|
return null
|
||||||
]}
|
}
|
||||||
tickFormatter={(val) => {
|
|
||||||
const { value, unit } = formatBytes(val, true, userSettings.unitDisk, false)
|
const title = extraFsName ? t`${extraFsName} I/O` : t`Disk I/O`
|
||||||
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
|
const description = extraFsName ? t`Throughput of ${extraFsName}` : t`Throughput of root filesystem`
|
||||||
}}
|
|
||||||
contentFormatter={({ value }) => {
|
const hasMoreIOMetrics = chartData.systemStats?.some((record) => record.stats?.dios?.at(0))
|
||||||
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, false)
|
|
||||||
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
|
let CornerEl = maxValSelect
|
||||||
}}
|
if (hasMoreIOMetrics) {
|
||||||
showTotal={true}
|
CornerEl = (
|
||||||
/>
|
<div className="flex gap-2">
|
||||||
</ChartCard>
|
{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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
257
internal/site/src/components/routes/system/disk-io-sheet.tsx
Normal file
257
internal/site/src/components/routes/system/disk-io-sheet.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -28,6 +28,8 @@ import type {
|
|||||||
import { $router, navigate } from "../../router"
|
import { $router, navigate } from "../../router"
|
||||||
import { appendData, cache, getStats, getTimeData, makeContainerData, makeContainerPoint } from "./chart-data"
|
import { appendData, cache, getStats, getTimeData, makeContainerData, makeContainerPoint } from "./chart-data"
|
||||||
|
|
||||||
|
export type SystemData = ReturnType<typeof useSystemData>
|
||||||
|
|
||||||
export function useSystemData(id: string) {
|
export function useSystemData(id: string) {
|
||||||
const direction = useStore($direction)
|
const direction = useStore($direction)
|
||||||
const systems = useStore($systems)
|
const systems = useStore($systems)
|
||||||
|
|||||||
232
internal/site/src/types.d.ts
vendored
232
internal/site/src/types.d.ts
vendored
@@ -124,6 +124,10 @@ export interface SystemStats {
|
|||||||
dio?: [number, number]
|
dio?: [number, number]
|
||||||
/** max disk I/O bytes [read, write] */
|
/** max disk I/O bytes [read, write] */
|
||||||
diom?: [number, number]
|
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) */
|
/** network sent (mb) */
|
||||||
ns: number
|
ns: number
|
||||||
/** network received (mb) */
|
/** network received (mb) */
|
||||||
@@ -186,6 +190,10 @@ export interface ExtraFsStats {
|
|||||||
rbm: number
|
rbm: number
|
||||||
/** max write per second (mb) */
|
/** max write per second (mb) */
|
||||||
wbm: number
|
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 {
|
export interface ContainerStatsRecord extends RecordModel {
|
||||||
@@ -413,118 +421,118 @@ export interface SystemdRecord extends RecordModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SystemdServiceDetails {
|
export interface SystemdServiceDetails {
|
||||||
AccessSELinuxContext: string;
|
AccessSELinuxContext: string
|
||||||
ActivationDetails: any[];
|
ActivationDetails: any[]
|
||||||
ActiveEnterTimestamp: number;
|
ActiveEnterTimestamp: number
|
||||||
ActiveEnterTimestampMonotonic: number;
|
ActiveEnterTimestampMonotonic: number
|
||||||
ActiveExitTimestamp: number;
|
ActiveExitTimestamp: number
|
||||||
ActiveExitTimestampMonotonic: number;
|
ActiveExitTimestampMonotonic: number
|
||||||
ActiveState: string;
|
ActiveState: string
|
||||||
After: string[];
|
After: string[]
|
||||||
AllowIsolate: boolean;
|
AllowIsolate: boolean
|
||||||
AssertResult: boolean;
|
AssertResult: boolean
|
||||||
AssertTimestamp: number;
|
AssertTimestamp: number
|
||||||
AssertTimestampMonotonic: number;
|
AssertTimestampMonotonic: number
|
||||||
Asserts: any[];
|
Asserts: any[]
|
||||||
Before: string[];
|
Before: string[]
|
||||||
BindsTo: any[];
|
BindsTo: any[]
|
||||||
BoundBy: any[];
|
BoundBy: any[]
|
||||||
CPUUsageNSec: number;
|
CPUUsageNSec: number
|
||||||
CanClean: any[];
|
CanClean: any[]
|
||||||
CanFreeze: boolean;
|
CanFreeze: boolean
|
||||||
CanIsolate: boolean;
|
CanIsolate: boolean
|
||||||
CanLiveMount: boolean;
|
CanLiveMount: boolean
|
||||||
CanReload: boolean;
|
CanReload: boolean
|
||||||
CanStart: boolean;
|
CanStart: boolean
|
||||||
CanStop: boolean;
|
CanStop: boolean
|
||||||
CollectMode: string;
|
CollectMode: string
|
||||||
ConditionResult: boolean;
|
ConditionResult: boolean
|
||||||
ConditionTimestamp: number;
|
ConditionTimestamp: number
|
||||||
ConditionTimestampMonotonic: number;
|
ConditionTimestampMonotonic: number
|
||||||
Conditions: any[];
|
Conditions: any[]
|
||||||
ConflictedBy: any[];
|
ConflictedBy: any[]
|
||||||
Conflicts: string[];
|
Conflicts: string[]
|
||||||
ConsistsOf: any[];
|
ConsistsOf: any[]
|
||||||
DebugInvocation: boolean;
|
DebugInvocation: boolean
|
||||||
DefaultDependencies: boolean;
|
DefaultDependencies: boolean
|
||||||
Description: string;
|
Description: string
|
||||||
Documentation: string[];
|
Documentation: string[]
|
||||||
DropInPaths: any[];
|
DropInPaths: any[]
|
||||||
ExecMainPID: number;
|
ExecMainPID: number
|
||||||
FailureAction: string;
|
FailureAction: string
|
||||||
FailureActionExitStatus: number;
|
FailureActionExitStatus: number
|
||||||
Following: string;
|
Following: string
|
||||||
FragmentPath: string;
|
FragmentPath: string
|
||||||
FreezerState: string;
|
FreezerState: string
|
||||||
Id: string;
|
Id: string
|
||||||
IgnoreOnIsolate: boolean;
|
IgnoreOnIsolate: boolean
|
||||||
InactiveEnterTimestamp: number;
|
InactiveEnterTimestamp: number
|
||||||
InactiveEnterTimestampMonotonic: number;
|
InactiveEnterTimestampMonotonic: number
|
||||||
InactiveExitTimestamp: number;
|
InactiveExitTimestamp: number
|
||||||
InactiveExitTimestampMonotonic: number;
|
InactiveExitTimestampMonotonic: number
|
||||||
InvocationID: string;
|
InvocationID: string
|
||||||
Job: Array<number | string>;
|
Job: Array<number | string>
|
||||||
JobRunningTimeoutUSec: number;
|
JobRunningTimeoutUSec: number
|
||||||
JobTimeoutAction: string;
|
JobTimeoutAction: string
|
||||||
JobTimeoutRebootArgument: string;
|
JobTimeoutRebootArgument: string
|
||||||
JobTimeoutUSec: number;
|
JobTimeoutUSec: number
|
||||||
JoinsNamespaceOf: any[];
|
JoinsNamespaceOf: any[]
|
||||||
LoadError: string[];
|
LoadError: string[]
|
||||||
LoadState: string;
|
LoadState: string
|
||||||
MainPID: number;
|
MainPID: number
|
||||||
Markers: any[];
|
Markers: any[]
|
||||||
MemoryCurrent: number;
|
MemoryCurrent: number
|
||||||
MemoryLimit: number;
|
MemoryLimit: number
|
||||||
MemoryPeak: number;
|
MemoryPeak: number
|
||||||
NRestarts: number;
|
NRestarts: number
|
||||||
Names: string[];
|
Names: string[]
|
||||||
NeedDaemonReload: boolean;
|
NeedDaemonReload: boolean
|
||||||
OnFailure: any[];
|
OnFailure: any[]
|
||||||
OnFailureJobMode: string;
|
OnFailureJobMode: string
|
||||||
OnFailureOf: any[];
|
OnFailureOf: any[]
|
||||||
OnSuccess: any[];
|
OnSuccess: any[]
|
||||||
OnSuccessJobMode: string;
|
OnSuccessJobMode: string
|
||||||
OnSuccessOf: any[];
|
OnSuccessOf: any[]
|
||||||
PartOf: any[];
|
PartOf: any[]
|
||||||
Perpetual: boolean;
|
Perpetual: boolean
|
||||||
PropagatesReloadTo: any[];
|
PropagatesReloadTo: any[]
|
||||||
PropagatesStopTo: any[];
|
PropagatesStopTo: any[]
|
||||||
RebootArgument: string;
|
RebootArgument: string
|
||||||
Refs: any[];
|
Refs: any[]
|
||||||
RefuseManualStart: boolean;
|
RefuseManualStart: boolean
|
||||||
RefuseManualStop: boolean;
|
RefuseManualStop: boolean
|
||||||
ReloadPropagatedFrom: any[];
|
ReloadPropagatedFrom: any[]
|
||||||
RequiredBy: any[];
|
RequiredBy: any[]
|
||||||
Requires: string[];
|
Requires: string[]
|
||||||
RequiresMountsFor: any[];
|
RequiresMountsFor: any[]
|
||||||
Requisite: any[];
|
Requisite: any[]
|
||||||
RequisiteOf: any[];
|
RequisiteOf: any[]
|
||||||
Result: string;
|
Result: string
|
||||||
SliceOf: any[];
|
SliceOf: any[]
|
||||||
SourcePath: string;
|
SourcePath: string
|
||||||
StartLimitAction: string;
|
StartLimitAction: string
|
||||||
StartLimitBurst: number;
|
StartLimitBurst: number
|
||||||
StartLimitIntervalUSec: number;
|
StartLimitIntervalUSec: number
|
||||||
StateChangeTimestamp: number;
|
StateChangeTimestamp: number
|
||||||
StateChangeTimestampMonotonic: number;
|
StateChangeTimestampMonotonic: number
|
||||||
StopPropagatedFrom: any[];
|
StopPropagatedFrom: any[]
|
||||||
StopWhenUnneeded: boolean;
|
StopWhenUnneeded: boolean
|
||||||
SubState: string;
|
SubState: string
|
||||||
SuccessAction: string;
|
SuccessAction: string
|
||||||
SuccessActionExitStatus: number;
|
SuccessActionExitStatus: number
|
||||||
SurviveFinalKillSignal: boolean;
|
SurviveFinalKillSignal: boolean
|
||||||
TasksCurrent: number;
|
TasksCurrent: number
|
||||||
TasksMax: number;
|
TasksMax: number
|
||||||
Transient: boolean;
|
Transient: boolean
|
||||||
TriggeredBy: string[];
|
TriggeredBy: string[]
|
||||||
Triggers: any[];
|
Triggers: any[]
|
||||||
UnitFilePreset: string;
|
UnitFilePreset: string
|
||||||
UnitFileState: string;
|
UnitFileState: string
|
||||||
UpheldBy: any[];
|
UpheldBy: any[]
|
||||||
Upholds: any[];
|
Upholds: any[]
|
||||||
WantedBy: any[];
|
WantedBy: any[]
|
||||||
Wants: string[];
|
Wants: string[]
|
||||||
WantsMountsFor: any[];
|
WantsMountsFor: any[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BeszelInfo {
|
export interface BeszelInfo {
|
||||||
|
|||||||
Reference in New Issue
Block a user