mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-22 05:36:15 +01:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2284c3fed | ||
|
|
eb420bef3a | ||
|
|
9cf6c167b0 | ||
|
|
fbc7f79660 | ||
|
|
37170f2bdb | ||
|
|
af4c05e692 | ||
|
|
202a506485 | ||
|
|
aa3866c8ed | ||
|
|
f9c0d0b89d | ||
|
|
ec5b1a833d | ||
|
|
1cfda8fb9f | ||
|
|
2168db6ebd | ||
|
|
e64ef49e97 | ||
|
|
54e0240dd8 | ||
|
|
05f52ad15a | ||
|
|
8ffb3a0cc8 | ||
|
|
953d7cac1e | ||
|
|
1cfd3cdd30 | ||
|
|
b4a3cb9ce6 | ||
|
|
7a6fbc8346 | ||
|
|
76cffb16de | ||
|
|
13f7d016e6 | ||
|
|
7a8dccfc97 | ||
|
|
68824935e9 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ dist
|
|||||||
beszel/cmd/hub/hub
|
beszel/cmd/hub/hub
|
||||||
beszel/cmd/agent/agent
|
beszel/cmd/agent/agent
|
||||||
node_modules
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ COPY internal ./internal
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
ARG TARGETOS TARGETARCH
|
ARG TARGETOS TARGETARCH
|
||||||
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./cmd/agent
|
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./cmd/agent
|
||||||
|
|
||||||
# ? -------------------------
|
# ? -------------------------
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ RUN update-ca-certificates
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
ARG TARGETOS TARGETARCH
|
ARG TARGETOS TARGETARCH
|
||||||
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel ./cmd/hub
|
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel ./cmd/hub
|
||||||
|
|
||||||
# ? -------------------------
|
# ? -------------------------
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ require (
|
|||||||
github.com/goccy/go-json v0.10.3
|
github.com/goccy/go-json v0.10.3
|
||||||
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
|
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
|
||||||
github.com/pocketbase/dbx v1.10.1
|
github.com/pocketbase/dbx v1.10.1
|
||||||
github.com/pocketbase/pocketbase v0.22.19
|
github.com/pocketbase/pocketbase v0.22.20
|
||||||
github.com/rhysd/go-github-selfupdate v1.2.3
|
github.com/rhysd/go-github-selfupdate v1.2.3
|
||||||
github.com/shirou/gopsutil/v4 v4.24.7
|
github.com/shirou/gopsutil/v4 v4.24.8
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
golang.org/x/crypto v0.26.0
|
golang.org/x/crypto v0.26.0
|
||||||
)
|
)
|
||||||
@@ -19,24 +19,24 @@ require (
|
|||||||
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2 v1.30.4 // indirect
|
github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.27.28 // indirect
|
github.com/aws/aws-sdk-go-v2/config v1.27.32 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.28 // indirect
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.31 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.12 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.60.0 // indirect
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.61.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.22.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.4 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.30.6 // indirect
|
||||||
github.com/aws/smithy-go v1.20.4 // indirect
|
github.com/aws/smithy-go v1.20.4 // indirect
|
||||||
github.com/disintegration/imaging v1.6.2 // indirect
|
github.com/disintegration/imaging v1.6.2 // indirect
|
||||||
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
|
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
|
||||||
@@ -84,13 +84,13 @@ require (
|
|||||||
golang.org/x/term v0.23.0 // indirect
|
golang.org/x/term v0.23.0 // indirect
|
||||||
golang.org/x/text v0.17.0 // indirect
|
golang.org/x/text v0.17.0 // indirect
|
||||||
golang.org/x/time v0.6.0 // indirect
|
golang.org/x/time v0.6.0 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||||
google.golang.org/api v0.194.0 // indirect
|
google.golang.org/api v0.196.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||||
google.golang.org/grpc v1.65.0 // indirect
|
google.golang.org/grpc v1.66.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect
|
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect
|
||||||
modernc.org/libc v1.59.9 // indirect
|
modernc.org/libc v1.60.1 // indirect
|
||||||
modernc.org/mathutil v1.6.0 // indirect
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
modernc.org/memory v1.8.0 // indirect
|
modernc.org/memory v1.8.0 // indirect
|
||||||
modernc.org/sqlite v1.32.0 // indirect
|
modernc.org/sqlite v1.32.0 // indirect
|
||||||
|
|||||||
128
beszel/go.sum
128
beszel/go.sum
@@ -1,8 +1,8 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=
|
cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=
|
||||||
cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
|
cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
|
||||||
cloud.google.com/go/auth v0.9.1 h1:+pMtLEV2k0AXKvs/tGZojuj6QaioxfUjOpMsG5Gtx+w=
|
cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U=
|
||||||
cloud.google.com/go/auth v0.9.1/go.mod h1:Sw8ocT5mhhXxFklyhT12Eiy0ed6tTrPMCJjSI8KhYLk=
|
cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
|
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
|
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
|
||||||
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
|
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
|
||||||
@@ -25,42 +25,42 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
|
|||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||||
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
|
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
|
||||||
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8=
|
github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
|
github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 h1:70PVAiL15/aBMh5LThwgXdSQorVr91L127ttckI9QQU=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 h1:70PVAiL15/aBMh5LThwgXdSQorVr91L127ttckI9QQU=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4/go.mod h1:/MQxMqci8tlqDH+pjmoLu1i0tbWCUP1hhyMRuFxpQCw=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4/go.mod h1:/MQxMqci8tlqDH+pjmoLu1i0tbWCUP1hhyMRuFxpQCw=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.27.28 h1:OTxWGW/91C61QlneCtnD62NLb4W616/NM1jA8LhJqbg=
|
github.com/aws/aws-sdk-go-v2/config v1.27.32 h1:jnAMVTJTpAQlePCUUlnXnllHEMGVWmvUJOiGjgtS9S0=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.27.28/go.mod h1:uzVRVtJSU5EFv6Fu82AoVFKozJi2ZCY6WRCXj06rbvs=
|
github.com/aws/aws-sdk-go-v2/config v1.27.32/go.mod h1:JibtzKJoXT0M/MhoYL6qfCk7nm/MppwukDFZtdgVRoY=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.28 h1:m8+AHY/ND8CMHJnPoH7PJIRakWGa4gbfbxuY9TGTUXM=
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.31 h1:jtyfcOfgoqWA2hW/E8sFbwdfgwD3APnF9CLCKE8dTyw=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.28/go.mod h1:6TF7dSc78ehD1SL6KpRIPKMA1GyyWflIkjqg+qmf4+c=
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.31/go.mod h1:RSgY5lfCfw+FoyKWtOpLolPlfQVdDBQWTUniAaE+NKY=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 h1:yjwoSyDZF8Jth+mUk5lSPJCkMC0lMy6FaCD51jm6ayE=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12/go.mod h1:fuR57fAgMk7ot3WcNQfb6rSEn+SUffl7ri+aa8uKysI=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.12 h1:i7cJ1izNlox4ka6cvbHPTztYGtbpW4Je/jyQIKOIU4A=
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.17 h1:QbV9wh6vtB3UAZvdfktPj8jT+w6yIrKYd4PngLWDmCE=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.12/go.mod h1:lHnam/4CTEVHaANZD54IrpE80VLK+lUU84WEeJ1FJ8M=
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.17/go.mod h1:0trBfk2z3LEozr2WZz7IxcRJWl2jv0Ro7JpByqh3coQ=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16/go.mod h1:2DwJF39FlNAUiX5pAc0UNeiz16lK2t7IaFcm0LFHEgc=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 h1:jYfy8UPmd+6kJW5YhY0L1/KftReOGxI/4NtVSTh9O/I=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16/go.mod h1:7ZfEPZxkW42Afq4uQB8H2E2e6ebh6mXTueEpYzjCzcs=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16 h1:mimdLQkIX1zr8GIPY1ZtALdBQGxcASiBd2MOp8m/dMc=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 h1:Roo69qTpfu8OlJ2Tb7pAYVuF0CpuUMB0IYWwYP/4DZM=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16/go.mod h1:YHk6owoSwrIsok+cAH9PENCOGoH5PU2EllX4vLtSrsY=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17/go.mod h1:NcWPxQzGM1USQggaTVwz6VpqMZPX1CvDJLDh6jnOCa4=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18 h1:GckUnpm4EJOAio1c8o25a+b3lVfwVzC9gnSBqiiNmZM=
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 h1:FLMkfEiRjhgeDTCjjLoc3URo/TBkgeQbocA78lfkzSI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18/go.mod h1:Br6+bxfG33Dk3ynmkhsW2Z/t9D4+lRqdLDNCKi85w0U=
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19/go.mod h1:Vx+GucNSsdhaxs3aZIKfSUjKVGsxN25nX2SRcdhuw08=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18/go.mod h1:++NHzT+nAF7ZPrHPsA+ENvsXkOO8wEu+C6RXltAG4/c=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16 h1:jg16PhLPUiHIj8zYIW6bqzeQSuHVEiWnGA0Brz5Xv2I=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 h1:u+EfGmksnJc/x5tq3A+OD7LrMbSSR/5TrKLvkdy/fhY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16/go.mod h1:Uyk1zE1VVdsHSU7096h/rwnXDzOzYQVl+FNPhPw7ShY=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17/go.mod h1:VaMx6302JHax2vHJWgRo+5n9zvbacs3bLU/23DNQrTY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.60.0 h1:2QXGJvG19QwqXUvgcdoCOZPyLuvZf8LiXPCN4P53TdI=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.61.1 h1:6ZRIbdMbN83W2/EIAU5z8FQZpmuULsBojTaok+uBEIg=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.60.0/go.mod h1:BSPI0EfnYUuNHPS0uqIo5VrRwzie+Fp+YhQOUs16sKI=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.61.1/go.mod h1:5FmD/Dqq57gP+XwaUnd5WFPipAuzrf0HmupX27Gvjvc=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 h1:zCsFCKvbj25i7p1u94imVoO447I/sFv8qq+lGJhRN0c=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.22.6 h1:o++HUDXlbrTl4PSal3YHtdErQxB8mDGAtkKNXBWPfIU=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.5/go.mod h1:ZeDX1SnKsVlejeuz41GiajjZpRSWR7/42q/EyA/QEiM=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.22.6/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 h1:SKvPgvdvmiTWoi0GAJ7AsJfOz3ngVkD/ERbs5pUnHNI=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6 h1:yCHcQCOwTfIsc8DoEhM3qXPxD+j8CbI6t1K3dNzsWV0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5/go.mod h1:20sz31hv/WsPa3HhU3hfrIet2kxM4Pe0r20eBZ20Tac=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.4 h1:iAckBT2OeEK/kBDyN/jDtpEExhjeeA/Im2q4X0rJZT8=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.30.6 h1:TrQadF7GcqvQ63kgwEcjlrVc2Fa0wpgLT0xtc73uAd8=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.4/go.mod h1:vmSqFK+BVIwVpDAGZB3CoCXHzurt4qBE8lf+I/kRTh0=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.30.6/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o=
|
||||||
github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
|
github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
|
||||||
github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||||
@@ -144,8 +144,8 @@ github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQF
|
|||||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0 h1:e+8XbKB6IMn8A4OAyZccO4pYfB3s7bt6azNIPE7AnPg=
|
github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA=
|
||||||
github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||||
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
|
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
|
||||||
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
|
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
@@ -153,8 +153,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
|
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
|
||||||
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
|
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.3 h1:QRje2j5GZimBzlbhGA2V2QlGNgL8G6e+wGo/+/2bWI0=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.3/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
|
||||||
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
|
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
|
||||||
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
|
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
@@ -202,8 +202,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pocketbase/dbx v1.10.1 h1:cw+vsyfCJD8YObOVeqb93YErnlxwYMkNZ4rwN0G0AaA=
|
github.com/pocketbase/dbx v1.10.1 h1:cw+vsyfCJD8YObOVeqb93YErnlxwYMkNZ4rwN0G0AaA=
|
||||||
github.com/pocketbase/dbx v1.10.1/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
github.com/pocketbase/dbx v1.10.1/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
||||||
github.com/pocketbase/pocketbase v0.22.19 h1:Hu9J2nsRQIaw8MiDLzE65xUPyMPjf4DcS2f+QmH1G+c=
|
github.com/pocketbase/pocketbase v0.22.20 h1:yUkhO5bTPWlzD4ZK6EQlS4R3AcHKDlBD+DxxU2BR83I=
|
||||||
github.com/pocketbase/pocketbase v0.22.19/go.mod h1:0QFvDOOW7ANId78ChZSagyHbmP6CgMxDQrQFXzeaDpA=
|
github.com/pocketbase/pocketbase v0.22.20/go.mod h1:Cw5E4uoGhKItBIE2lJL3NfmiUr9Syk2xaNJ2G7Dssow=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
@@ -214,8 +214,8 @@ github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzx
|
|||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/shirou/gopsutil/v4 v4.24.7 h1:V9UGTK4gQ8HvcnPKf6Zt3XHyQq/peaekfxpJ2HSocJk=
|
github.com/shirou/gopsutil/v4 v4.24.8 h1:pVQjIenQkIhqO81mwTaXjTzOMT7d3TZkf43PlVFHENI=
|
||||||
github.com/shirou/gopsutil/v4 v4.24.7/go.mod h1:0uW/073rP7FYLOkvxolUQM5rMOLTNmRXnFKafpb71rw=
|
github.com/shirou/gopsutil/v4 v4.24.8/go.mod h1:wE0OrJtj4dG+hYkxqDH3QiBICdKSf04/npcvLLc/oRg=
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||||
@@ -254,16 +254,16 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
|
|||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
||||||
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
||||||
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
|
||||||
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
|
||||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
|
||||||
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
||||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
||||||
gocloud.dev v0.39.0 h1:EYABYGhAalPUaMrbSKOr5lejxoxvXj99nE8XFtsDgds=
|
gocloud.dev v0.39.0 h1:EYABYGhAalPUaMrbSKOr5lejxoxvXj99nE8XFtsDgds=
|
||||||
gocloud.dev v0.39.0/go.mod h1:drz+VyYNBvrMTW0KZiBAYEdl8lbNZx+OQ7oQvdrFmSQ=
|
gocloud.dev v0.39.0/go.mod h1:drz+VyYNBvrMTW0KZiBAYEdl8lbNZx+OQ7oQvdrFmSQ=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
@@ -349,10 +349,10 @@ golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
|||||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk=
|
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||||
google.golang.org/api v0.194.0 h1:dztZKG9HgtIpbI35FhfuSNR/zmaMVdxNlntHj1sIS4s=
|
google.golang.org/api v0.196.0 h1:k/RafYqebaIJBO3+SMnfEGtFVlvp5vSgqTUF54UN/zg=
|
||||||
google.golang.org/api v0.194.0/go.mod h1:AgvUFdojGANh3vI+P7EVnxj3AISHllxGCJSFmggmnd0=
|
google.golang.org/api v0.196.0/go.mod h1:g9IL21uGkYgvQ5BZg6BAtoGJQIm8r6EgaAbpNey5wBE=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
@@ -360,19 +360,19 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID
|
|||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142 h1:oLiyxGgE+rt22duwci1+TG7bg2/L1LQsXwfjPlmuJA0=
|
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU=
|
||||||
google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142/go.mod h1:G11eXq53iI5Q+kyNOmCvnzBaxEA2Q/Ik5Tj7nqBE8j4=
|
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240812133136-8ffd90a71988 h1:+/tmTy5zAieooKIXfzDm9KiA3Bv6JBwriRN9LY+yayk=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240812133136-8ffd90a71988 h1:+/tmTy5zAieooKIXfzDm9KiA3Bv6JBwriRN9LY+yayk=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240812133136-8ffd90a71988/go.mod h1:4+X6GvPs+25wZKbQq9qyAXrwIRExv7w0Ea6MgZLZiDM=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240812133136-8ffd90a71988/go.mod h1:4+X6GvPs+25wZKbQq9qyAXrwIRExv7w0Ea6MgZLZiDM=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c=
|
||||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
@@ -398,16 +398,16 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
|
|||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||||
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||||
modernc.org/ccgo/v4 v4.20.7 h1:skrinQsjxWfvj6nbC3ztZPJy+NuwmB3hV9zX/pthNYQ=
|
modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
|
||||||
modernc.org/ccgo/v4 v4.20.7/go.mod h1:UOkI3JSG2zT4E2ioHlncSOZsXbuDCZLvPi3uMlZT5GY=
|
modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0=
|
||||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
|
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
|
||||||
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||||
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbPMxoTpgAJeyePh0SmO8M=
|
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbPMxoTpgAJeyePh0SmO8M=
|
||||||
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||||
modernc.org/libc v1.59.9 h1:k+nNDDakwipimgmJ1D9H466LhFeSkaPPycAs1OZiDmY=
|
modernc.org/libc v1.60.1 h1:at373l8IFRTkJIkAU85BIuUoBM4T1b51ds0E1ovPG2s=
|
||||||
modernc.org/libc v1.59.9/go.mod h1:EY/egGEU7Ju66eU6SBqCNYaFUDuc4npICkMWnU5EE3A=
|
modernc.org/libc v1.60.1/go.mod h1:xJuobKuNxKH3RUatS7GjR+suWj+5c2K7bi4m/S5arOY=
|
||||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||||
|
|||||||
@@ -36,10 +36,11 @@ type Agent struct {
|
|||||||
sem chan struct{}
|
sem chan struct{}
|
||||||
containerStatsMap map[string]*container.PrevContainerStats
|
containerStatsMap map[string]*container.PrevContainerStats
|
||||||
containerStatsMutex *sync.Mutex
|
containerStatsMutex *sync.Mutex
|
||||||
diskIoStats *system.DiskIoStats
|
fsNames []string
|
||||||
|
fsStats map[string]*system.FsStats
|
||||||
|
netInterfaces map[string]struct{}
|
||||||
netIoStats *system.NetIoStats
|
netIoStats *system.NetIoStats
|
||||||
dockerClient *http.Client
|
dockerClient *http.Client
|
||||||
containerStatsPool *sync.Pool
|
|
||||||
bufferPool *sync.Pool
|
bufferPool *sync.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,14 +51,8 @@ func NewAgent(pubKey []byte, addr string) *Agent {
|
|||||||
sem: make(chan struct{}, 15),
|
sem: make(chan struct{}, 15),
|
||||||
containerStatsMap: make(map[string]*container.PrevContainerStats),
|
containerStatsMap: make(map[string]*container.PrevContainerStats),
|
||||||
containerStatsMutex: &sync.Mutex{},
|
containerStatsMutex: &sync.Mutex{},
|
||||||
diskIoStats: &system.DiskIoStats{},
|
|
||||||
netIoStats: &system.NetIoStats{},
|
netIoStats: &system.NetIoStats{},
|
||||||
dockerClient: newDockerClient(),
|
dockerClient: newDockerClient(),
|
||||||
containerStatsPool: &sync.Pool{
|
|
||||||
New: func() interface{} {
|
|
||||||
return new(container.Stats)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
bufferPool: &sync.Pool{
|
bufferPool: &sync.Pool{
|
||||||
New: func() interface{} {
|
New: func() interface{} {
|
||||||
return new(bytes.Buffer)
|
return new(bytes.Buffer)
|
||||||
@@ -74,8 +69,8 @@ func (a *Agent) releaseSemaphore() {
|
|||||||
<-a.sem
|
<-a.sem
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) getSystemStats() (*system.Info, *system.Stats) {
|
func (a *Agent) getSystemStats() (system.Info, system.Stats) {
|
||||||
systemStats := &system.Stats{}
|
systemStats := system.Stats{}
|
||||||
|
|
||||||
// cpu percent
|
// cpu percent
|
||||||
cpuPct, err := cpu.Percent(0, false)
|
cpuPct, err := cpu.Percent(0, false)
|
||||||
@@ -96,34 +91,59 @@ func (a *Agent) getSystemStats() (*system.Info, *system.Stats) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// disk usage
|
// disk usage
|
||||||
if d, err := disk.Usage("/"); err == nil {
|
for _, stats := range a.fsStats {
|
||||||
systemStats.Disk = bytesToGigabytes(d.Total)
|
// log.Println("Reading filesystem:", fs.Mountpoint)
|
||||||
|
if d, err := disk.Usage(stats.Mountpoint); err == nil {
|
||||||
|
stats.DiskTotal = bytesToGigabytes(d.Total)
|
||||||
|
stats.DiskUsed = bytesToGigabytes(d.Used)
|
||||||
|
if stats.Root {
|
||||||
|
systemStats.DiskTotal = bytesToGigabytes(d.Total)
|
||||||
systemStats.DiskUsed = bytesToGigabytes(d.Used)
|
systemStats.DiskUsed = bytesToGigabytes(d.Used)
|
||||||
systemStats.DiskPct = twoDecimals(d.UsedPercent)
|
systemStats.DiskPct = twoDecimals(d.UsedPercent)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// reset stats if error (likely unmounted)
|
||||||
|
log.Printf("Error reading %s: %+v\n", stats.Mountpoint, err)
|
||||||
|
stats.DiskTotal = 0
|
||||||
|
stats.DiskUsed = 0
|
||||||
|
stats.TotalRead = 0
|
||||||
|
stats.TotalWrite = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// disk i/o
|
// disk i/o
|
||||||
if io, err := disk.IOCounters(a.diskIoStats.Filesystem); err == nil {
|
if ioCounters, err := disk.IOCounters(a.fsNames...); err == nil {
|
||||||
for _, d := range io {
|
for _, d := range ioCounters {
|
||||||
// add to systemStats
|
stats := a.fsStats[d.Name]
|
||||||
secondsElapsed := time.Since(a.diskIoStats.Time).Seconds()
|
if stats == nil {
|
||||||
readPerSecond := float64(d.ReadBytes-a.diskIoStats.Read) / secondsElapsed
|
continue
|
||||||
systemStats.DiskRead = bytesToMegabytes(readPerSecond)
|
}
|
||||||
writePerSecond := float64(d.WriteBytes-a.diskIoStats.Write) / secondsElapsed
|
secondsElapsed := time.Since(stats.Time).Seconds()
|
||||||
systemStats.DiskWrite = bytesToMegabytes(writePerSecond)
|
readPerSecond := float64(d.ReadBytes-stats.TotalRead) / secondsElapsed
|
||||||
// update diskIoStats
|
writePerSecond := float64(d.WriteBytes-stats.TotalWrite) / secondsElapsed
|
||||||
a.diskIoStats.Time = time.Now()
|
stats.Time = time.Now()
|
||||||
a.diskIoStats.Read = d.ReadBytes
|
stats.DiskReadPs = bytesToMegabytes(readPerSecond)
|
||||||
a.diskIoStats.Write = d.WriteBytes
|
stats.DiskWritePs = bytesToMegabytes(writePerSecond)
|
||||||
|
stats.TotalRead = d.ReadBytes
|
||||||
|
stats.TotalWrite = d.WriteBytes
|
||||||
|
// if root filesystem, update system stats
|
||||||
|
if stats.Root {
|
||||||
|
systemStats.DiskReadPs = stats.DiskReadPs
|
||||||
|
systemStats.DiskWritePs = stats.DiskWritePs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// network stats
|
// network stats
|
||||||
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
||||||
|
secondsElapsed := time.Since(a.netIoStats.Time).Seconds()
|
||||||
|
a.netIoStats.Time = time.Now()
|
||||||
bytesSent := uint64(0)
|
bytesSent := uint64(0)
|
||||||
bytesRecv := uint64(0)
|
bytesRecv := uint64(0)
|
||||||
|
// sum all bytes sent and received
|
||||||
for _, v := range netIO {
|
for _, v := range netIO {
|
||||||
if skipNetworkInterface(&v) {
|
// skip if not in valid network interfaces list
|
||||||
|
if _, exists := a.netInterfaces[v.Name]; !exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// log.Printf("%+v: %+v recv, %+v sent\n", v.Name, v.BytesRecv, v.BytesSent)
|
// log.Printf("%+v: %+v recv, %+v sent\n", v.Name, v.BytesRecv, v.BytesSent)
|
||||||
@@ -131,15 +151,28 @@ func (a *Agent) getSystemStats() (*system.Info, *system.Stats) {
|
|||||||
bytesRecv += v.BytesRecv
|
bytesRecv += v.BytesRecv
|
||||||
}
|
}
|
||||||
// add to systemStats
|
// add to systemStats
|
||||||
secondsElapsed := time.Since(a.netIoStats.Time).Seconds()
|
|
||||||
sentPerSecond := float64(bytesSent-a.netIoStats.BytesSent) / secondsElapsed
|
sentPerSecond := float64(bytesSent-a.netIoStats.BytesSent) / secondsElapsed
|
||||||
recvPerSecond := float64(bytesRecv-a.netIoStats.BytesRecv) / secondsElapsed
|
recvPerSecond := float64(bytesRecv-a.netIoStats.BytesRecv) / secondsElapsed
|
||||||
systemStats.NetworkSent = bytesToMegabytes(sentPerSecond)
|
networkSentPs := bytesToMegabytes(sentPerSecond)
|
||||||
systemStats.NetworkRecv = bytesToMegabytes(recvPerSecond)
|
networkRecvPs := bytesToMegabytes(recvPerSecond)
|
||||||
|
// add check for issue (#150) where sent is a massive number
|
||||||
|
if networkSentPs > 10_000 || networkRecvPs > 10_000 {
|
||||||
|
log.Printf("Warning: network sent/recv is %.2f/%.2f MB/s. Resetting stats.\n", networkSentPs, networkRecvPs)
|
||||||
|
for _, v := range netIO {
|
||||||
|
if _, exists := a.netInterfaces[v.Name]; !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf("%+s: %v recv, %v sent\n", v.Name, v.BytesRecv, v.BytesSent)
|
||||||
|
}
|
||||||
|
// reset network I/O stats
|
||||||
|
a.initializeNetIoStats()
|
||||||
|
} else {
|
||||||
|
systemStats.NetworkSent = networkSentPs
|
||||||
|
systemStats.NetworkRecv = networkRecvPs
|
||||||
// update netIoStats
|
// update netIoStats
|
||||||
a.netIoStats.BytesSent = bytesSent
|
a.netIoStats.BytesSent = bytesSent
|
||||||
a.netIoStats.BytesRecv = bytesRecv
|
a.netIoStats.BytesRecv = bytesRecv
|
||||||
a.netIoStats.Time = time.Now()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// temperatures
|
// temperatures
|
||||||
@@ -157,7 +190,7 @@ func (a *Agent) getSystemStats() (*system.Info, *system.Stats) {
|
|||||||
// log.Printf("Temperature map: %+v\n", systemStats.Temperatures)
|
// log.Printf("Temperature map: %+v\n", systemStats.Temperatures)
|
||||||
}
|
}
|
||||||
|
|
||||||
systemInfo := &system.Info{
|
systemInfo := system.Info{
|
||||||
Cpu: systemStats.Cpu,
|
Cpu: systemStats.Cpu,
|
||||||
MemPct: systemStats.MemPct,
|
MemPct: systemStats.MemPct,
|
||||||
DiskPct: systemStats.DiskPct,
|
DiskPct: systemStats.DiskPct,
|
||||||
@@ -183,7 +216,7 @@ func (a *Agent) getSystemStats() (*system.Info, *system.Stats) {
|
|||||||
return systemInfo, systemStats
|
return systemInfo, systemStats
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) getDockerStats() ([]*container.Stats, error) {
|
func (a *Agent) getDockerStats() ([]container.Stats, error) {
|
||||||
resp, err := a.dockerClient.Get("http://localhost/containers/json")
|
resp, err := a.dockerClient.Get("http://localhost/containers/json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.closeIdleConnections(err)
|
a.closeIdleConnections(err)
|
||||||
@@ -191,13 +224,14 @@ func (a *Agent) getDockerStats() ([]*container.Stats, error) {
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
var containers []*container.ApiInfo
|
var containers []container.ApiInfo
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil {
|
||||||
log.Printf("Error decoding containers: %+v\n", err)
|
log.Printf("Error decoding containers: %+v\n", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
containerStats := make([]*container.Stats, 0, len(containers))
|
containerStats := make([]container.Stats, 0, len(containers))
|
||||||
|
containerStatsMutex := sync.Mutex{}
|
||||||
|
|
||||||
// store valid ids to clean up old container ids from map
|
// store valid ids to clean up old container ids from map
|
||||||
validIds := make(map[string]struct{}, len(containers))
|
validIds := make(map[string]struct{}, len(containers))
|
||||||
@@ -214,7 +248,9 @@ func (a *Agent) getDockerStats() ([]*container.Stats, error) {
|
|||||||
a.deleteContainerStatsSync(ctr.IdShort)
|
a.deleteContainerStatsSync(ctr.IdShort)
|
||||||
}
|
}
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
a.acquireSemaphore()
|
||||||
go func() {
|
go func() {
|
||||||
|
defer a.releaseSemaphore()
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
cstats, err := a.getContainerStats(ctr)
|
cstats, err := a.getContainerStats(ctr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -231,6 +267,8 @@ func (a *Agent) getDockerStats() ([]*container.Stats, error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
containerStatsMutex.Lock()
|
||||||
|
defer containerStatsMutex.Unlock()
|
||||||
containerStats = append(containerStats, cstats)
|
containerStats = append(containerStats, cstats)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -247,38 +285,35 @@ func (a *Agent) getDockerStats() ([]*container.Stats, error) {
|
|||||||
return containerStats, nil
|
return containerStats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) getContainerStats(ctr *container.ApiInfo) (*container.Stats, error) {
|
func (a *Agent) getContainerStats(ctr container.ApiInfo) (container.Stats, error) {
|
||||||
// use semaphore to limit concurrency
|
cStats := container.Stats{}
|
||||||
a.acquireSemaphore()
|
|
||||||
defer a.releaseSemaphore()
|
|
||||||
|
|
||||||
resp, err := a.dockerClient.Get("http://localhost/containers/" + ctr.IdShort + "/stats?stream=0&one-shot=1")
|
resp, err := a.dockerClient.Get("http://localhost/containers/" + ctr.IdShort + "/stats?stream=0&one-shot=1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return cStats, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// get a buffer from the pool
|
// use a pooled buffer to store the response body
|
||||||
buf := a.bufferPool.Get().(*bytes.Buffer)
|
buf := a.bufferPool.Get().(*bytes.Buffer)
|
||||||
defer a.bufferPool.Put(buf)
|
defer a.bufferPool.Put(buf)
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
// read the response body into the buffer
|
|
||||||
_, err = io.Copy(buf, resp.Body)
|
_, err = io.Copy(buf, resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return cStats, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// unmarshal the json data from the buffer
|
// unmarshal the json data from the buffer
|
||||||
var statsJson container.ApiStats
|
var statsJson container.ApiStats
|
||||||
if err := json.Unmarshal(buf.Bytes(), &statsJson); err != nil {
|
if err := json.Unmarshal(buf.Bytes(), &statsJson); err != nil {
|
||||||
return nil, err
|
return cStats, err
|
||||||
}
|
}
|
||||||
|
|
||||||
name := ctr.Names[0][1:]
|
name := ctr.Names[0][1:]
|
||||||
|
|
||||||
// check if container has valid data, otherwise may be in restart loop (#103)
|
// check if container has valid data, otherwise may be in restart loop (#103)
|
||||||
if statsJson.MemoryStats.Usage == 0 {
|
if statsJson.MemoryStats.Usage == 0 {
|
||||||
return nil, fmt.Errorf("%s - invalid data", name)
|
return cStats, fmt.Errorf("%s - invalid data", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// memory (https://docs.docker.com/reference/cli/docker/container/stats/)
|
// memory (https://docs.docker.com/reference/cli/docker/container/stats/)
|
||||||
@@ -303,7 +338,7 @@ func (a *Agent) getContainerStats(ctr *container.ApiInfo) (*container.Stats, err
|
|||||||
systemDelta := statsJson.CPUStats.SystemUsage - stats.Cpu[1]
|
systemDelta := statsJson.CPUStats.SystemUsage - stats.Cpu[1]
|
||||||
cpuPct := float64(cpuDelta) / float64(systemDelta) * 100
|
cpuPct := float64(cpuDelta) / float64(systemDelta) * 100
|
||||||
if cpuPct > 100 {
|
if cpuPct > 100 {
|
||||||
return nil, fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct)
|
return cStats, fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct)
|
||||||
}
|
}
|
||||||
stats.Cpu = [2]uint64{statsJson.CPUStats.CPUUsage.TotalUsage, statsJson.CPUStats.SystemUsage}
|
stats.Cpu = [2]uint64{statsJson.CPUStats.CPUUsage.TotalUsage, statsJson.CPUStats.SystemUsage}
|
||||||
|
|
||||||
@@ -325,7 +360,7 @@ func (a *Agent) getContainerStats(ctr *container.ApiInfo) (*container.Stats, err
|
|||||||
stats.Net.Recv = total_recv
|
stats.Net.Recv = total_recv
|
||||||
stats.Net.Time = time.Now()
|
stats.Net.Time = time.Now()
|
||||||
|
|
||||||
cStats := a.containerStatsPool.Get().(*container.Stats)
|
// cStats := a.containerStatsPool.Get().(*container.Stats)
|
||||||
cStats.Name = name
|
cStats.Name = name
|
||||||
cStats.Cpu = twoDecimals(cpuPct)
|
cStats.Cpu = twoDecimals(cpuPct)
|
||||||
cStats.Mem = bytesToMegabytes(float64(usedMemory))
|
cStats.Mem = bytesToMegabytes(float64(usedMemory))
|
||||||
@@ -342,24 +377,25 @@ func (a *Agent) deleteContainerStatsSync(id string) {
|
|||||||
delete(a.containerStatsMap, id)
|
delete(a.containerStatsMap, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) gatherStats() *system.CombinedData {
|
func (a *Agent) gatherStats() system.CombinedData {
|
||||||
systemInfo, systemStats := a.getSystemStats()
|
systemInfo, systemStats := a.getSystemStats()
|
||||||
systemData := &system.CombinedData{
|
systemData := system.CombinedData{
|
||||||
Stats: systemStats,
|
Stats: systemStats,
|
||||||
Info: systemInfo,
|
Info: systemInfo,
|
||||||
}
|
}
|
||||||
|
// add docker stats
|
||||||
if containerStats, err := a.getDockerStats(); err == nil {
|
if containerStats, err := a.getDockerStats(); err == nil {
|
||||||
systemData.Containers = containerStats
|
systemData.Containers = containerStats
|
||||||
}
|
}
|
||||||
// fmt.Printf("%+v\n", systemData)
|
// add extra filesystems
|
||||||
return systemData
|
systemData.Stats.ExtraFs = make(map[string]*system.FsStats)
|
||||||
}
|
for name, stats := range a.fsStats {
|
||||||
|
if !stats.Root && stats.DiskTotal > 0 {
|
||||||
// return container stats to pool
|
systemData.Stats.ExtraFs[name] = stats
|
||||||
func (a *Agent) returnStatsToPool(containerStats []*container.Stats) {
|
|
||||||
for _, stats := range containerStats {
|
|
||||||
a.containerStatsPool.Put(stats)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// log.Printf("%+v\n", systemData)
|
||||||
|
return systemData
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) startServer() {
|
func (a *Agent) startServer() {
|
||||||
@@ -378,7 +414,6 @@ func (a *Agent) startServer() {
|
|||||||
|
|
||||||
func (a *Agent) handleSession(s sshServer.Session) {
|
func (a *Agent) handleSession(s sshServer.Session) {
|
||||||
stats := a.gatherStats()
|
stats := a.gatherStats()
|
||||||
defer a.returnStatsToPool(stats.Containers)
|
|
||||||
encoder := json.NewEncoder(s)
|
encoder := json.NewEncoder(s)
|
||||||
if err := encoder.Encode(stats); err != nil {
|
if err := encoder.Encode(stats); err != nil {
|
||||||
log.Println("Error encoding stats:", err.Error())
|
log.Println("Error encoding stats:", err.Error())
|
||||||
@@ -389,43 +424,126 @@ func (a *Agent) handleSession(s sshServer.Session) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) Run() {
|
func (a *Agent) Run() {
|
||||||
if filesystem, exists := os.LookupEnv("FILESYSTEM"); exists {
|
a.fsStats = make(map[string]*system.FsStats)
|
||||||
a.diskIoStats.Filesystem = filesystem
|
|
||||||
} else {
|
filesystem, fsEnvVarExists := os.LookupEnv("FILESYSTEM")
|
||||||
a.diskIoStats.Filesystem = findDefaultFilesystem()
|
if fsEnvVarExists {
|
||||||
|
a.fsStats[filesystem] = &system.FsStats{Root: true, Mountpoint: "/"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if extraFilesystems, exists := os.LookupEnv("EXTRA_FILESYSTEMS"); exists {
|
||||||
|
// parse comma separated list of filesystems
|
||||||
|
for _, filesystem := range strings.Split(extraFilesystems, ",") {
|
||||||
|
a.fsStats[filesystem] = &system.FsStats{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.initializeDiskInfo(fsEnvVarExists)
|
||||||
a.initializeDiskIoStats()
|
a.initializeDiskIoStats()
|
||||||
a.initializeNetIoStats()
|
a.initializeNetIoStats()
|
||||||
|
|
||||||
|
// log.Printf("Filesystems: %+v\n", a.fsStats)
|
||||||
a.startServer()
|
a.startServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sets up the filesystems to monitor for disk usage and I/O.
|
||||||
|
func (a *Agent) initializeDiskInfo(fsEnvVarExists bool) error {
|
||||||
|
partitions, err := disk.Partitions(false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// log.Printf("Partitions: %+v\n", partitions)
|
||||||
|
for _, v := range partitions {
|
||||||
|
// binary - use root mountpoint if not already set by env var
|
||||||
|
if !fsEnvVarExists && v.Mountpoint == "/" {
|
||||||
|
a.fsStats[v.Device] = &system.FsStats{Root: true, Mountpoint: "/"}
|
||||||
|
}
|
||||||
|
// docker - use /etc/hosts device as root if not mapped
|
||||||
|
if !fsEnvVarExists && v.Mountpoint == "/etc/hosts" && strings.HasPrefix(v.Device, "/dev") && !strings.Contains(v.Device, "mapper") {
|
||||||
|
a.fsStats[v.Device] = &system.FsStats{Root: true, Mountpoint: "/"}
|
||||||
|
}
|
||||||
|
// check if device is in /extra-filesystem
|
||||||
|
if strings.HasPrefix(v.Mountpoint, "/extra-filesystem") {
|
||||||
|
// todo: may be able to tweak this to be able to mount custom root at /extra-filesystems/root
|
||||||
|
a.fsStats[v.Device] = &system.FsStats{Mountpoint: v.Mountpoint}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// set mountpoints for extra filesystems if passed in via env var
|
||||||
|
for name, stats := range a.fsStats {
|
||||||
|
if strings.HasSuffix(v.Device, name) {
|
||||||
|
stats.Mountpoint = v.Mountpoint
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove extra filesystems that don't have a mountpoint
|
||||||
|
hasRoot := false
|
||||||
|
for name, stats := range a.fsStats {
|
||||||
|
if stats.Root {
|
||||||
|
hasRoot = true
|
||||||
|
log.Println("Detected root fs:", name)
|
||||||
|
}
|
||||||
|
if stats.Mountpoint == "" {
|
||||||
|
log.Printf("Ignoring %s. No mountpoint found.\n", name)
|
||||||
|
delete(a.fsStats, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no root filesystem set, use most read device in /proc/diskstats
|
||||||
|
if !hasRoot {
|
||||||
|
rootDevice := findMaxReadsDevice()
|
||||||
|
log.Printf("Detected root fs: %+s\n", rootDevice)
|
||||||
|
a.fsStats[rootDevice] = &system.FsStats{Root: true, Mountpoint: "/"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets start values for disk I/O stats.
|
||||||
func (a *Agent) initializeDiskIoStats() {
|
func (a *Agent) initializeDiskIoStats() {
|
||||||
if io, err := disk.IOCounters(a.diskIoStats.Filesystem); err == nil {
|
// create slice of fs names to pass to disk.IOCounters later
|
||||||
|
a.fsNames = make([]string, 0, len(a.fsStats))
|
||||||
|
|
||||||
|
for name, stats := range a.fsStats {
|
||||||
|
if io, err := disk.IOCounters(name); err == nil {
|
||||||
for _, d := range io {
|
for _, d := range io {
|
||||||
a.diskIoStats.Time = time.Now()
|
// add name to slice
|
||||||
a.diskIoStats.Read = d.ReadBytes
|
a.fsNames = append(a.fsNames, d.Name)
|
||||||
a.diskIoStats.Write = d.WriteBytes
|
// normalize name with io counters
|
||||||
|
if name != d.Name {
|
||||||
|
// log.Println("Normalizing disk I/O stats:", name, d.Name)
|
||||||
|
a.fsStats[d.Name] = stats
|
||||||
|
delete(a.fsStats, name)
|
||||||
|
}
|
||||||
|
stats.Time = time.Now()
|
||||||
|
stats.TotalRead = d.ReadBytes
|
||||||
|
stats.TotalWrite = d.WriteBytes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) initializeNetIoStats() {
|
func (a *Agent) initializeNetIoStats() {
|
||||||
|
// reset valid network interfaces
|
||||||
|
a.netInterfaces = make(map[string]struct{}, 0)
|
||||||
|
// reset network I/O stats
|
||||||
|
a.netIoStats.BytesSent = 0
|
||||||
|
a.netIoStats.BytesRecv = 0
|
||||||
|
// get intial network I/O stats
|
||||||
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
||||||
bytesSent := uint64(0)
|
a.netIoStats.Time = time.Now()
|
||||||
bytesRecv := uint64(0)
|
|
||||||
for _, v := range netIO {
|
for _, v := range netIO {
|
||||||
if skipNetworkInterface(&v) {
|
if skipNetworkInterface(v) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Printf("Found network interface: %+v (%+v recv, %+v sent)\n", v.Name, v.BytesRecv, v.BytesSent)
|
log.Printf("Detected network interface: %+v (%+v recv, %+v sent)\n", v.Name, v.BytesRecv, v.BytesSent)
|
||||||
bytesSent += v.BytesSent
|
a.netIoStats.BytesSent += v.BytesSent
|
||||||
bytesRecv += v.BytesRecv
|
a.netIoStats.BytesRecv += v.BytesRecv
|
||||||
|
// store as a valid network interface
|
||||||
|
a.netInterfaces[v.Name] = struct{}{}
|
||||||
}
|
}
|
||||||
a.netIoStats.BytesSent = bytesSent
|
|
||||||
a.netIoStats.BytesRecv = bytesRecv
|
|
||||||
a.netIoStats.Time = time.Now()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,19 +559,7 @@ func twoDecimals(value float64) float64 {
|
|||||||
return math.Round(value*100) / 100
|
return math.Round(value*100) / 100
|
||||||
}
|
}
|
||||||
|
|
||||||
func findDefaultFilesystem() string {
|
func skipNetworkInterface(v psutilNet.IOCountersStat) bool {
|
||||||
if partitions, err := disk.Partitions(false); err == nil {
|
|
||||||
for _, v := range partitions {
|
|
||||||
if v.Mountpoint == "/" {
|
|
||||||
log.Printf("Using filesystem: %+v\n", v.Device)
|
|
||||||
return v.Device
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func skipNetworkInterface(v *psutilNet.IOCountersStat) bool {
|
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(v.Name, "lo"),
|
case strings.HasPrefix(v.Name, "lo"),
|
||||||
strings.HasPrefix(v.Name, "docker"),
|
strings.HasPrefix(v.Name, "docker"),
|
||||||
@@ -516,3 +622,40 @@ func (a *Agent) closeIdleConnections(err error) (isTimeout bool) {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the device with the most reads in /proc/diskstats
|
||||||
|
// (fallback in case the root device is not supplied or detected)
|
||||||
|
func findMaxReadsDevice() string {
|
||||||
|
content, err := os.ReadFile("/proc/diskstats")
|
||||||
|
if err != nil {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(content), "\n")
|
||||||
|
var maxReadsSectors int64
|
||||||
|
var maxReadsDevice string
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 7 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceName := fields[2]
|
||||||
|
readsSectors, err := strconv.ParseInt(fields[5], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if readsSectors > maxReadsSectors {
|
||||||
|
maxReadsSectors = readsSectors
|
||||||
|
maxReadsDevice = deviceName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxReadsDevice == "" {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxReadsDevice
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,13 +14,11 @@ import (
|
|||||||
|
|
||||||
type AlertManager struct {
|
type AlertManager struct {
|
||||||
app *pocketbase.PocketBase
|
app *pocketbase.PocketBase
|
||||||
mailClient mailer.Mailer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAlertManager(app *pocketbase.PocketBase) *AlertManager {
|
func NewAlertManager(app *pocketbase.PocketBase) *AlertManager {
|
||||||
return &AlertManager{
|
return &AlertManager{
|
||||||
app: app,
|
app: app,
|
||||||
mailClient: app.NewMailClient(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +143,7 @@ func (am *AlertManager) sendAlert(message *mailer.Message) {
|
|||||||
Address: am.app.Settings().Meta.SenderAddress,
|
Address: am.app.Settings().Meta.SenderAddress,
|
||||||
Name: am.app.Settings().Meta.SenderName,
|
Name: am.app.Settings().Meta.SenderName,
|
||||||
}
|
}
|
||||||
if err := am.mailClient.Send(message); err != nil {
|
if err := am.app.NewMailClient().Send(message); err != nil {
|
||||||
am.app.Logger().Error("Failed to send alert: ", "err", err.Error())
|
am.app.Logger().Error("Failed to send alert: ", "err", err.Error())
|
||||||
} else {
|
} else {
|
||||||
am.app.Logger().Info("Sent alert", "to", message.To, "subj", message.Subject)
|
am.app.Logger().Info("Sent alert", "to", message.To, "subj", message.Subject)
|
||||||
|
|||||||
@@ -11,23 +11,30 @@ type Stats struct {
|
|||||||
MemUsed float64 `json:"mu"`
|
MemUsed float64 `json:"mu"`
|
||||||
MemPct float64 `json:"mp"`
|
MemPct float64 `json:"mp"`
|
||||||
MemBuffCache float64 `json:"mb"`
|
MemBuffCache float64 `json:"mb"`
|
||||||
Swap float64 `json:"s"`
|
Swap float64 `json:"s,omitempty"`
|
||||||
SwapUsed float64 `json:"su"`
|
SwapUsed float64 `json:"su,omitempty"`
|
||||||
Disk float64 `json:"d"`
|
DiskTotal float64 `json:"d"`
|
||||||
DiskUsed float64 `json:"du"`
|
DiskUsed float64 `json:"du"`
|
||||||
DiskPct float64 `json:"dp"`
|
DiskPct float64 `json:"dp"`
|
||||||
DiskRead float64 `json:"dr"`
|
DiskReadPs float64 `json:"dr"`
|
||||||
DiskWrite float64 `json:"dw"`
|
DiskWritePs float64 `json:"dw"`
|
||||||
NetworkSent float64 `json:"ns"`
|
NetworkSent float64 `json:"ns"`
|
||||||
NetworkRecv float64 `json:"nr"`
|
NetworkRecv float64 `json:"nr"`
|
||||||
Temperatures map[string]float64 `json:"t,omitempty"`
|
Temperatures map[string]float64 `json:"t,omitempty"`
|
||||||
|
ExtraFs map[string]*FsStats `json:"efs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DiskIoStats struct {
|
type FsStats struct {
|
||||||
Read uint64
|
Time time.Time `json:"-"`
|
||||||
Write uint64
|
Device string `json:"-"`
|
||||||
Time time.Time
|
Root bool `json:"-"`
|
||||||
Filesystem string
|
Mountpoint string `json:"-"`
|
||||||
|
DiskTotal float64 `json:"d"`
|
||||||
|
DiskUsed float64 `json:"du"`
|
||||||
|
TotalRead uint64 `json:"-"`
|
||||||
|
TotalWrite uint64 `json:"-"`
|
||||||
|
DiskWritePs float64 `json:"w"`
|
||||||
|
DiskReadPs float64 `json:"r"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NetIoStats struct {
|
type NetIoStats struct {
|
||||||
@@ -51,7 +58,7 @@ type Info struct {
|
|||||||
|
|
||||||
// Final data structure to return to the hub
|
// Final data structure to return to the hub
|
||||||
type CombinedData struct {
|
type CombinedData struct {
|
||||||
Stats *Stats `json:"stats"`
|
Stats Stats `json:"stats"`
|
||||||
Info *Info `json:"info"`
|
Info Info `json:"info"`
|
||||||
Containers []*container.Stats `json:"container"`
|
Containers []container.Stats `json:"container"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"beszel/internal/entities/system"
|
"beszel/internal/entities/system"
|
||||||
"beszel/internal/records"
|
"beszel/internal/records"
|
||||||
"beszel/site"
|
"beszel/site"
|
||||||
|
"context"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -246,8 +247,10 @@ func (h *Hub) updateSystem(record *models.Record) {
|
|||||||
// create system connection
|
// create system connection
|
||||||
client, err = h.createSystemConnection(record)
|
client, err = h.createSystemConnection(record)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if record.GetString("status") != "down" {
|
||||||
h.app.Logger().Error("Failed to connect:", "err", err.Error(), "system", record.GetString("host"), "port", record.GetString("port"))
|
h.app.Logger().Error("Failed to connect:", "err", err.Error(), "system", record.GetString("host"), "port", record.GetString("port"))
|
||||||
h.updateSystemStatus(record, "down")
|
h.updateSystemStatus(record, "down")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.connectionLock.Lock()
|
h.connectionLock.Lock()
|
||||||
@@ -350,7 +353,7 @@ func (h *Hub) createSSHClientConfig() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func requestJsonFromAgent(client *ssh.Client, systemData *system.CombinedData) error {
|
func requestJsonFromAgent(client *ssh.Client, systemData *system.CombinedData) error {
|
||||||
session, err := client.NewSession()
|
session, err := newSessionWithTimeout(client, 5*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("bad client")
|
return fmt.Errorf("bad client")
|
||||||
}
|
}
|
||||||
@@ -377,6 +380,32 @@ func requestJsonFromAgent(client *ssh.Client, systemData *system.CombinedData) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adds timeout to SSH session creation to avoid hanging in case of network issues
|
||||||
|
func newSessionWithTimeout(client *ssh.Client, timeout time.Duration) (*ssh.Session, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// use goroutine to create the session
|
||||||
|
sessionChan := make(chan *ssh.Session, 1)
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
if session, err := client.NewSession(); err != nil {
|
||||||
|
errChan <- err
|
||||||
|
} else {
|
||||||
|
sessionChan <- session
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case session := <-sessionChan:
|
||||||
|
return session, nil
|
||||||
|
case err := <-errChan:
|
||||||
|
return nil, err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, fmt.Errorf("session creation timed out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Hub) getSSHKey() ([]byte, error) {
|
func (h *Hub) getSSHKey() ([]byte, error) {
|
||||||
dataDir := h.app.DataDir()
|
dataDir := h.app.DataDir()
|
||||||
// check if the key pair already exists
|
// check if the key pair already exists
|
||||||
|
|||||||
@@ -140,33 +140,11 @@ func (rm *RecordManager) CreateLongerRecords() {
|
|||||||
// log.Println("finished creating longer records", "time (ms)", time.Since(start).Milliseconds())
|
// log.Println("finished creating longer records", "time (ms)", time.Since(start).Milliseconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the average stats of a list of system_stats records with reflect
|
|
||||||
// func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Stats {
|
|
||||||
// count := float64(len(records))
|
|
||||||
// sum := reflect.New(reflect.TypeOf(system.Stats{})).Elem()
|
|
||||||
|
|
||||||
// var stats system.Stats
|
|
||||||
// for _, record := range records {
|
|
||||||
// record.UnmarshalJSONField("stats", &stats)
|
|
||||||
// statValue := reflect.ValueOf(stats)
|
|
||||||
// for i := 0; i < statValue.NumField(); i++ {
|
|
||||||
// field := sum.Field(i)
|
|
||||||
// field.SetFloat(field.Float() + statValue.Field(i).Float())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// average := reflect.New(reflect.TypeOf(system.Stats{})).Elem()
|
|
||||||
// for i := 0; i < sum.NumField(); i++ {
|
|
||||||
// average.Field(i).SetFloat(twoDecimals(sum.Field(i).Float() / count))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return average.Interface().(system.Stats)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Calculate the average stats of a list of system_stats records without reflect
|
// Calculate the average stats of a list of system_stats records without reflect
|
||||||
func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Stats {
|
func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Stats {
|
||||||
var sum system.Stats
|
var sum system.Stats
|
||||||
sum.Temperatures = make(map[string]float64)
|
sum.Temperatures = make(map[string]float64)
|
||||||
|
sum.ExtraFs = make(map[string]*system.FsStats)
|
||||||
|
|
||||||
count := float64(len(records))
|
count := float64(len(records))
|
||||||
// use different counter for temps in case some records don't have them
|
// use different counter for temps in case some records don't have them
|
||||||
@@ -182,13 +160,14 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
|
|||||||
sum.MemBuffCache += stats.MemBuffCache
|
sum.MemBuffCache += stats.MemBuffCache
|
||||||
sum.Swap += stats.Swap
|
sum.Swap += stats.Swap
|
||||||
sum.SwapUsed += stats.SwapUsed
|
sum.SwapUsed += stats.SwapUsed
|
||||||
sum.Disk += stats.Disk
|
sum.DiskTotal += stats.DiskTotal
|
||||||
sum.DiskUsed += stats.DiskUsed
|
sum.DiskUsed += stats.DiskUsed
|
||||||
sum.DiskPct += stats.DiskPct
|
sum.DiskPct += stats.DiskPct
|
||||||
sum.DiskRead += stats.DiskRead
|
sum.DiskReadPs += stats.DiskReadPs
|
||||||
sum.DiskWrite += stats.DiskWrite
|
sum.DiskWritePs += stats.DiskWritePs
|
||||||
sum.NetworkSent += stats.NetworkSent
|
sum.NetworkSent += stats.NetworkSent
|
||||||
sum.NetworkRecv += stats.NetworkRecv
|
sum.NetworkRecv += stats.NetworkRecv
|
||||||
|
// add temps to sum
|
||||||
if stats.Temperatures != nil {
|
if stats.Temperatures != nil {
|
||||||
tempCount++
|
tempCount++
|
||||||
for key, value := range stats.Temperatures {
|
for key, value := range stats.Temperatures {
|
||||||
@@ -198,6 +177,18 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
|
|||||||
sum.Temperatures[key] += value
|
sum.Temperatures[key] += value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// add extra fs to sum
|
||||||
|
if stats.ExtraFs != nil {
|
||||||
|
for key, value := range stats.ExtraFs {
|
||||||
|
if _, ok := sum.ExtraFs[key]; !ok {
|
||||||
|
sum.ExtraFs[key] = &system.FsStats{}
|
||||||
|
}
|
||||||
|
sum.ExtraFs[key].DiskTotal += value.DiskTotal
|
||||||
|
sum.ExtraFs[key].DiskUsed += value.DiskUsed
|
||||||
|
sum.ExtraFs[key].DiskWritePs += value.DiskWritePs
|
||||||
|
sum.ExtraFs[key].DiskReadPs += value.DiskReadPs
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stats = system.Stats{
|
stats = system.Stats{
|
||||||
@@ -208,11 +199,11 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
|
|||||||
MemBuffCache: twoDecimals(sum.MemBuffCache / count),
|
MemBuffCache: twoDecimals(sum.MemBuffCache / count),
|
||||||
Swap: twoDecimals(sum.Swap / count),
|
Swap: twoDecimals(sum.Swap / count),
|
||||||
SwapUsed: twoDecimals(sum.SwapUsed / count),
|
SwapUsed: twoDecimals(sum.SwapUsed / count),
|
||||||
Disk: twoDecimals(sum.Disk / count),
|
DiskTotal: twoDecimals(sum.DiskTotal / count),
|
||||||
DiskUsed: twoDecimals(sum.DiskUsed / count),
|
DiskUsed: twoDecimals(sum.DiskUsed / count),
|
||||||
DiskPct: twoDecimals(sum.DiskPct / count),
|
DiskPct: twoDecimals(sum.DiskPct / count),
|
||||||
DiskRead: twoDecimals(sum.DiskRead / count),
|
DiskReadPs: twoDecimals(sum.DiskReadPs / count),
|
||||||
DiskWrite: twoDecimals(sum.DiskWrite / count),
|
DiskWritePs: twoDecimals(sum.DiskWritePs / count),
|
||||||
NetworkSent: twoDecimals(sum.NetworkSent / count),
|
NetworkSent: twoDecimals(sum.NetworkSent / count),
|
||||||
NetworkRecv: twoDecimals(sum.NetworkRecv / count),
|
NetworkRecv: twoDecimals(sum.NetworkRecv / count),
|
||||||
}
|
}
|
||||||
@@ -224,6 +215,18 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(sum.ExtraFs) != 0 {
|
||||||
|
stats.ExtraFs = make(map[string]*system.FsStats)
|
||||||
|
for key, value := range sum.ExtraFs {
|
||||||
|
stats.ExtraFs[key] = &system.FsStats{
|
||||||
|
DiskTotal: twoDecimals(value.DiskTotal / count),
|
||||||
|
DiskUsed: twoDecimals(value.DiskUsed / count),
|
||||||
|
DiskWritePs: twoDecimals(value.DiskWritePs / count),
|
||||||
|
DiskReadPs: twoDecimals(value.DiskReadPs / count),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -5,12 +5,6 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Beszel</title>
|
<title>Beszel</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
4597
beszel/site/package-lock.json
generated
4597
beszel/site/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@
|
|||||||
"@radix-ui/react-switch": "^1.1.0",
|
"@radix-ui/react-switch": "^1.1.0",
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
"@tanstack/react-table": "^8.20.1",
|
"@tanstack/react-table": "^8.20.5",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -31,23 +31,23 @@
|
|||||||
"d3-time": "^3.1.0",
|
"d3-time": "^3.1.0",
|
||||||
"lucide-react": "^0.407.0",
|
"lucide-react": "^0.407.0",
|
||||||
"nanostores": "^0.10.3",
|
"nanostores": "^0.10.3",
|
||||||
"pocketbase": "^0.21.4",
|
"pocketbase": "^0.21.5",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"recharts": "^2.13.0-alpha.4",
|
"recharts": "^2.13.0-alpha.4",
|
||||||
"tailwind-merge": "^2.4.0",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"use-is-in-viewport": "^1.0.9",
|
"use-is-in-viewport": "^1.0.9",
|
||||||
"valibot": "^0.36.0"
|
"valibot": "^0.36.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.1.6",
|
"@types/bun": "^1.1.8",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.5",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"postcss": "^8.4.40",
|
"postcss": "^8.4.44",
|
||||||
"tailwindcss": "^3.4.7",
|
"tailwindcss": "^3.4.10",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.3.5"
|
"vite": "^5.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
beszel/site/public/static/InterVariable.woff2
Normal file
BIN
beszel/site/public/static/InterVariable.woff2
Normal file
Binary file not shown.
@@ -33,10 +33,12 @@ export function AddSystemButton({ className }: { className?: string }) {
|
|||||||
network_mode: host
|
network_mode: host
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
# monitor other disks / partitions by mounting a folder in /extra-filesystems
|
||||||
|
# - /mnt/disk1/.beszel:/extra-filesystems/disk1:ro
|
||||||
environment:
|
environment:
|
||||||
PORT: ${port}
|
PORT: ${port}
|
||||||
KEY: "${publicKey}"
|
KEY: "${publicKey}"
|
||||||
# FILESYSTEM: /dev/sda1 # set to the correct filesystem for disk I/O stats`)
|
# FILESYSTEM: /dev/sda1 # override the root partition / device for disk I/O stats`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit(e: SubmitEvent) {
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
cn,
|
cn,
|
||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
|
twoDecimalString,
|
||||||
useYaxisWidth,
|
useYaxisWidth,
|
||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
@@ -72,8 +73,8 @@ export default function BandwidthChart({
|
|||||||
animationDuration={150}
|
animationDuration={150}
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
unit=" MB/s"
|
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
|
contentFormatter={(item) => twoDecimalString(item.value) + ' MB/s'}
|
||||||
indicator="line"
|
indicator="line"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default function ChartTimeSelect({ className }: { className?: string }) {
|
|||||||
onValueChange={(value: ChartTimes) => $chartTime.set(value)}
|
onValueChange={(value: ChartTimes) => $chartTime.set(value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className={cn(className, 'relative pl-10 pr-5')}>
|
<SelectTrigger className={cn(className, 'relative pl-10 pr-5')}>
|
||||||
<HistoryIcon className="h-4 w-4 absolute left-4 top-1/2 -translate-y-1/2 opacity-80" />
|
<HistoryIcon className="h-4 w-4 absolute left-4 top-1/2 -translate-y-1/2 opacity-85" />
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import {
|
|||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from '@/components/ui/chart'
|
} from '@/components/ui/chart'
|
||||||
import { useMemo, useRef } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
|
import { chartTimeData, cn, formatShortDate, twoDecimalString, useYaxisWidth } from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $chartTime } from '@/lib/stores'
|
import { $chartTime, $containerFilter } from '@/lib/stores'
|
||||||
|
|
||||||
export default function ContainerCpuChart({
|
export default function ContainerCpuChart({
|
||||||
chartData,
|
chartData,
|
||||||
@@ -21,6 +21,7 @@ export default function ContainerCpuChart({
|
|||||||
const chartRef = useRef<HTMLDivElement>(null)
|
const chartRef = useRef<HTMLDivElement>(null)
|
||||||
const yAxisWidth = useYaxisWidth(chartRef)
|
const yAxisWidth = useYaxisWidth(chartRef)
|
||||||
const chartTime = useStore($chartTime)
|
const chartTime = useStore($chartTime)
|
||||||
|
const filter = useStore($containerFilter)
|
||||||
|
|
||||||
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
||||||
|
|
||||||
@@ -109,23 +110,33 @@ export default function ContainerCpuChart({
|
|||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
itemSorter={(a, b) => b.value - a.value}
|
itemSorter={(a, b) => b.value - a.value}
|
||||||
content={<ChartTooltipContent unit="%" indicator="line" />}
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
filter={filter}
|
||||||
|
contentFormatter={(item) => twoDecimalString(item.value) + '%'}
|
||||||
|
indicator="line"
|
||||||
/>
|
/>
|
||||||
{Object.keys(chartConfig).map((key) => (
|
}
|
||||||
|
/>
|
||||||
|
{Object.keys(chartConfig).map((key) => {
|
||||||
|
const filtered = filter && !key.includes(filter)
|
||||||
|
let fillOpacity = filtered ? 0.05 : 0.4
|
||||||
|
let strokeOpacity = filtered ? 0.1 : 1
|
||||||
|
return (
|
||||||
<Area
|
<Area
|
||||||
key={key}
|
key={key}
|
||||||
// isAnimationActive={chartData.length < 20}
|
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
// animateNewValues={false}
|
|
||||||
// animationDuration={1200}
|
|
||||||
dataKey={key}
|
dataKey={key}
|
||||||
type="monotoneX"
|
type="monotoneX"
|
||||||
fill={chartConfig[key].color}
|
fill={chartConfig[key].color}
|
||||||
fillOpacity={0.4}
|
fillOpacity={fillOpacity}
|
||||||
stroke={chartConfig[key].color}
|
stroke={chartConfig[key].color}
|
||||||
|
strokeOpacity={strokeOpacity}
|
||||||
|
activeDot={{ opacity: filtered ? 0 : 1 }}
|
||||||
stackId="a"
|
stackId="a"
|
||||||
/>
|
/>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ import {
|
|||||||
cn,
|
cn,
|
||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
|
twoDecimalString,
|
||||||
useYaxisWidth,
|
useYaxisWidth,
|
||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $chartTime } from '@/lib/stores'
|
import { $chartTime, $containerFilter } from '@/lib/stores'
|
||||||
|
|
||||||
export default function ContainerMemChart({
|
export default function ContainerMemChart({
|
||||||
chartData,
|
chartData,
|
||||||
@@ -27,6 +28,7 @@ export default function ContainerMemChart({
|
|||||||
const chartTime = useStore($chartTime)
|
const chartTime = useStore($chartTime)
|
||||||
const chartRef = useRef<HTMLDivElement>(null)
|
const chartRef = useRef<HTMLDivElement>(null)
|
||||||
const yAxisWidth = useYaxisWidth(chartRef)
|
const yAxisWidth = useYaxisWidth(chartRef)
|
||||||
|
const filter = useStore($containerFilter)
|
||||||
|
|
||||||
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
||||||
|
|
||||||
@@ -114,21 +116,33 @@ export default function ContainerMemChart({
|
|||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.time)}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
itemSorter={(a, b) => b.value - a.value}
|
itemSorter={(a, b) => b.value - a.value}
|
||||||
content={<ChartTooltipContent unit=" MB" indicator="line" />}
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
filter={filter}
|
||||||
|
contentFormatter={(item) => twoDecimalString(item.value) + ' MB'}
|
||||||
|
indicator="line"
|
||||||
/>
|
/>
|
||||||
{Object.keys(chartConfig).map((key) => (
|
}
|
||||||
|
/>
|
||||||
|
{Object.keys(chartConfig).map((key) => {
|
||||||
|
const filtered = filter && !key.includes(filter)
|
||||||
|
let fillOpacity = filtered ? 0.05 : 0.4
|
||||||
|
let strokeOpacity = filtered ? 0.1 : 1
|
||||||
|
return (
|
||||||
<Area
|
<Area
|
||||||
key={key}
|
key={key}
|
||||||
// animationDuration={1200}
|
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
dataKey={key}
|
dataKey={key}
|
||||||
type="monotoneX"
|
type="monotoneX"
|
||||||
fill={chartConfig[key].color}
|
fill={chartConfig[key].color}
|
||||||
fillOpacity={0.4}
|
strokeOpacity={strokeOpacity}
|
||||||
|
fillOpacity={fillOpacity}
|
||||||
stroke={chartConfig[key].color}
|
stroke={chartConfig[key].color}
|
||||||
|
activeDot={filtered ? false : {}}
|
||||||
stackId="a"
|
stackId="a"
|
||||||
/>
|
/>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ import {
|
|||||||
cn,
|
cn,
|
||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
|
twoDecimalString,
|
||||||
useYaxisWidth,
|
useYaxisWidth,
|
||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $chartTime } from '@/lib/stores'
|
import { $chartTime, $containerFilter } from '@/lib/stores'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
|
||||||
export default function ContainerCpuChart({
|
export default function ContainerCpuChart({
|
||||||
@@ -28,6 +29,7 @@ export default function ContainerCpuChart({
|
|||||||
const chartTime = useStore($chartTime)
|
const chartTime = useStore($chartTime)
|
||||||
const chartRef = useRef<HTMLDivElement>(null)
|
const chartRef = useRef<HTMLDivElement>(null)
|
||||||
const yAxisWidth = useYaxisWidth(chartRef)
|
const yAxisWidth = useYaxisWidth(chartRef)
|
||||||
|
const filter = useStore($containerFilter)
|
||||||
|
|
||||||
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
||||||
|
|
||||||
@@ -116,6 +118,7 @@ export default function ContainerCpuChart({
|
|||||||
itemSorter={(a, b) => b.value - a.value}
|
itemSorter={(a, b) => b.value - a.value}
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
|
filter={filter}
|
||||||
indicator="line"
|
indicator="line"
|
||||||
contentFormatter={(item, key) => {
|
contentFormatter={(item, key) => {
|
||||||
try {
|
try {
|
||||||
@@ -123,10 +126,10 @@ export default function ContainerCpuChart({
|
|||||||
const received = item?.payload?.[key][1] ?? 0
|
const received = item?.payload?.[key][1] ?? 0
|
||||||
return (
|
return (
|
||||||
<span className="flex">
|
<span className="flex">
|
||||||
{received.toLocaleString()} MB/s
|
{twoDecimalString(received)} MB/s
|
||||||
<span className="opacity-70 ml-0.5"> rx </span>
|
<span className="opacity-70 ml-0.5"> rx </span>
|
||||||
<Separator orientation="vertical" className="h-3 mx-1.5 bg-primary/40" />
|
<Separator orientation="vertical" className="h-3 mx-1.5 bg-primary/40" />
|
||||||
{sent.toLocaleString()} MB/s<span className="opacity-70 ml-0.5"> tx</span>
|
{twoDecimalString(sent)} MB/s<span className="opacity-70 ml-0.5"> tx</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -136,7 +139,11 @@ export default function ContainerCpuChart({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{Object.keys(chartConfig).map((key) => (
|
{Object.keys(chartConfig).map((key) => {
|
||||||
|
const filtered = filter && !key.includes(filter)
|
||||||
|
let fillOpacity = filtered ? 0.05 : 0.4
|
||||||
|
let strokeOpacity = filtered ? 0.1 : 1
|
||||||
|
return (
|
||||||
<Area
|
<Area
|
||||||
key={key}
|
key={key}
|
||||||
name={key}
|
name={key}
|
||||||
@@ -145,11 +152,14 @@ export default function ContainerCpuChart({
|
|||||||
dataKey={(data) => data?.[key]?.[2] ?? 0}
|
dataKey={(data) => data?.[key]?.[2] ?? 0}
|
||||||
type="monotoneX"
|
type="monotoneX"
|
||||||
fill={chartConfig[key].color}
|
fill={chartConfig[key].color}
|
||||||
fillOpacity={0.4}
|
fillOpacity={fillOpacity}
|
||||||
stroke={chartConfig[key].color}
|
stroke={chartConfig[key].color}
|
||||||
|
strokeOpacity={strokeOpacity}
|
||||||
|
activeDot={{ opacity: filtered ? 0 : 1 }}
|
||||||
stackId="a"
|
stackId="a"
|
||||||
/>
|
/>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
||||||
|
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
||||||
import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
|
import { chartTimeData, cn, formatShortDate, twoDecimalString, useYaxisWidth } from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $chartTime } from '@/lib/stores'
|
import { $chartTime } from '@/lib/stores'
|
||||||
@@ -60,8 +60,8 @@ export default function CpuChart({
|
|||||||
animationDuration={150}
|
animationDuration={150}
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
unit="%"
|
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
|
contentFormatter={(item) => twoDecimalString(item.value) + '%'}
|
||||||
indicator="line"
|
indicator="line"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
||||||
|
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
||||||
import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
|
import { chartTimeData, cn, formatShortDate, twoDecimalString, useYaxisWidth } from '@/lib/utils'
|
||||||
import { useMemo, useRef } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
@@ -22,22 +22,9 @@ export default function DiskChart({
|
|||||||
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
||||||
|
|
||||||
const diskSize = useMemo(() => {
|
const diskSize = useMemo(() => {
|
||||||
return Math.round(systemData[0]?.stats.d)
|
return Math.round(systemData.at(-1)?.stats.d ?? NaN)
|
||||||
}, [systemData])
|
}, [systemData])
|
||||||
|
|
||||||
// const ticks = useMemo(() => {
|
|
||||||
// let ticks = [0]
|
|
||||||
// for (let i = 1; i < diskSize; i += diskSize / 5) {
|
|
||||||
// ticks.push(Math.trunc(i))
|
|
||||||
// }
|
|
||||||
// ticks.push(diskSize)
|
|
||||||
// return ticks
|
|
||||||
// }, [diskSize])
|
|
||||||
|
|
||||||
// if (!systemData.length || !ticks.length) {
|
|
||||||
// return <Spinner />
|
|
||||||
// }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={chartRef}>
|
<div ref={chartRef}>
|
||||||
{/* {!yAxisSet && <Spinner />} */}
|
{/* {!yAxisSet && <Spinner />} */}
|
||||||
@@ -63,6 +50,7 @@ export default function DiskChart({
|
|||||||
width={yAxisWidth}
|
width={yAxisWidth}
|
||||||
domain={[0, diskSize]}
|
domain={[0, diskSize]}
|
||||||
tickCount={9}
|
tickCount={9}
|
||||||
|
minTickGap={6}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
unit={' GB'}
|
unit={' GB'}
|
||||||
@@ -83,8 +71,8 @@ export default function DiskChart({
|
|||||||
animationDuration={150}
|
animationDuration={150}
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
unit=" GB"
|
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
|
contentFormatter={(item) => twoDecimalString(item.value) + ' GB'}
|
||||||
indicator="line"
|
indicator="line"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
cn,
|
cn,
|
||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
|
twoDecimalString,
|
||||||
useYaxisWidth,
|
useYaxisWidth,
|
||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
@@ -76,8 +77,8 @@ export default function DiskIoChart({
|
|||||||
animationDuration={150}
|
animationDuration={150}
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
unit=" MB/s"
|
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
|
contentFormatter={(item) => twoDecimalString(item.value) + ' MB/s'}
|
||||||
indicator="line"
|
indicator="line"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
92
beszel/site/src/components/charts/extra-fs-disk-chart.tsx
Normal file
92
beszel/site/src/components/charts/extra-fs-disk-chart.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
||||||
|
|
||||||
|
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
||||||
|
import { chartTimeData, cn, formatShortDate, twoDecimalString, useYaxisWidth } from '@/lib/utils'
|
||||||
|
import { useMemo, useRef } from 'react'
|
||||||
|
// import Spinner from '../spinner'
|
||||||
|
import { useStore } from '@nanostores/react'
|
||||||
|
import { $chartTime } from '@/lib/stores'
|
||||||
|
import { SystemStatsRecord } from '@/types'
|
||||||
|
|
||||||
|
export default function ExFsDiskChart({
|
||||||
|
ticks,
|
||||||
|
systemData,
|
||||||
|
fs,
|
||||||
|
}: {
|
||||||
|
ticks: number[]
|
||||||
|
systemData: SystemStatsRecord[]
|
||||||
|
fs: string
|
||||||
|
}) {
|
||||||
|
const chartTime = useStore($chartTime)
|
||||||
|
const chartRef = useRef<HTMLDivElement>(null)
|
||||||
|
const yAxisWidth = useYaxisWidth(chartRef)
|
||||||
|
|
||||||
|
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
||||||
|
|
||||||
|
const diskSize = useMemo(() => {
|
||||||
|
const size = systemData.at(-1)?.stats.efs?.[fs].d ?? 0
|
||||||
|
return size > 10 ? Math.round(size) : size
|
||||||
|
}, [systemData])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={chartRef}>
|
||||||
|
<ChartContainer
|
||||||
|
config={{}}
|
||||||
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
|
'opacity-100': yAxisSet,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<AreaChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={systemData}
|
||||||
|
margin={{
|
||||||
|
top: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<YAxis
|
||||||
|
className="tracking-tighter"
|
||||||
|
width={yAxisWidth}
|
||||||
|
domain={[0, diskSize]}
|
||||||
|
tickCount={9}
|
||||||
|
minTickGap={6}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
unit={' GB'}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="created"
|
||||||
|
domain={[ticks[0], ticks.at(-1)!]}
|
||||||
|
ticks={ticks}
|
||||||
|
type="number"
|
||||||
|
scale={'time'}
|
||||||
|
minTickGap={35}
|
||||||
|
tickMargin={8}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={chartTimeData[chartTime].format}
|
||||||
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
animationEasing="ease-out"
|
||||||
|
animationDuration={150}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
|
contentFormatter={(item) => twoDecimalString(item.value) + ' GB'}
|
||||||
|
indicator="line"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey={`stats.efs.${fs}.du`}
|
||||||
|
name="Disk Usage"
|
||||||
|
type="monotoneX"
|
||||||
|
fill="hsl(var(--chart-4))"
|
||||||
|
fillOpacity={0.4}
|
||||||
|
stroke="hsl(var(--chart-4))"
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
112
beszel/site/src/components/charts/extra-fs-disk-io-chart.tsx
Normal file
112
beszel/site/src/components/charts/extra-fs-disk-io-chart.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
||||||
|
|
||||||
|
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
||||||
|
import {
|
||||||
|
chartTimeData,
|
||||||
|
cn,
|
||||||
|
formatShortDate,
|
||||||
|
toFixedWithoutTrailingZeros,
|
||||||
|
twoDecimalString,
|
||||||
|
useYaxisWidth,
|
||||||
|
} from '@/lib/utils'
|
||||||
|
// import Spinner from '../spinner'
|
||||||
|
import { useStore } from '@nanostores/react'
|
||||||
|
import { $chartTime } from '@/lib/stores'
|
||||||
|
import { SystemStatsRecord } from '@/types'
|
||||||
|
import { useMemo, useRef } from 'react'
|
||||||
|
|
||||||
|
export default function ExFsDiskIoChart({
|
||||||
|
ticks,
|
||||||
|
systemData,
|
||||||
|
fs,
|
||||||
|
}: {
|
||||||
|
ticks: number[]
|
||||||
|
systemData: SystemStatsRecord[]
|
||||||
|
fs: string
|
||||||
|
}) {
|
||||||
|
const chartTime = useStore($chartTime)
|
||||||
|
const chartRef = useRef<HTMLDivElement>(null)
|
||||||
|
const yAxisWidth = useYaxisWidth(chartRef)
|
||||||
|
|
||||||
|
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
||||||
|
|
||||||
|
// if (!systemData.length || !ticks.length) {
|
||||||
|
// return <Spinner />
|
||||||
|
// }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={chartRef}>
|
||||||
|
{/* {!yAxisSet && <Spinner />} */}
|
||||||
|
<ChartContainer
|
||||||
|
config={{}}
|
||||||
|
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
|
||||||
|
'opacity-100': yAxisSet,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<AreaChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={systemData}
|
||||||
|
margin={{
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 10,
|
||||||
|
bottom: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<YAxis
|
||||||
|
className="tracking-tighter"
|
||||||
|
width={yAxisWidth}
|
||||||
|
// domain={[0, (max: number) => (max <= 0.4 ? 0.4 : Math.ceil(max))]}
|
||||||
|
tickFormatter={(value) => toFixedWithoutTrailingZeros(value, 2)}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
unit={' MB/s'}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="created"
|
||||||
|
domain={[ticks[0], ticks.at(-1)!]}
|
||||||
|
ticks={ticks}
|
||||||
|
type="number"
|
||||||
|
scale={'time'}
|
||||||
|
minTickGap={35}
|
||||||
|
tickMargin={8}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={chartTimeData[chartTime].format}
|
||||||
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
animationEasing="ease-out"
|
||||||
|
animationDuration={150}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
|
contentFormatter={(item) => twoDecimalString(item.value) + ' MB/s'}
|
||||||
|
indicator="line"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey={`stats.efs.${fs}.w`}
|
||||||
|
name="Write"
|
||||||
|
type="monotoneX"
|
||||||
|
fill="hsl(var(--chart-3))"
|
||||||
|
fillOpacity={0.3}
|
||||||
|
stroke="hsl(var(--chart-3))"
|
||||||
|
// animationDuration={1200}
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
dataKey={`stats.efs.${fs}.r`}
|
||||||
|
name="Read"
|
||||||
|
type="monotoneX"
|
||||||
|
fill="hsl(var(--chart-1))"
|
||||||
|
fillOpacity={0.3}
|
||||||
|
stroke="hsl(var(--chart-1))"
|
||||||
|
// animationDuration={1200}
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
||||||
|
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
||||||
import { chartTimeData, cn, formatShortDate, toFixedFloat, useYaxisWidth } from '@/lib/utils'
|
import {
|
||||||
|
chartTimeData,
|
||||||
|
cn,
|
||||||
|
formatShortDate,
|
||||||
|
toFixedFloat,
|
||||||
|
twoDecimalString,
|
||||||
|
useYaxisWidth,
|
||||||
|
} from '@/lib/utils'
|
||||||
import { useMemo, useRef } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
@@ -71,10 +78,10 @@ export default function MemChart({
|
|||||||
animationDuration={150}
|
animationDuration={150}
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
unit=" GB"
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
itemSorter={(a, b) => a.name.localeCompare(b.name)}
|
itemSorter={(a, b) => a.name.localeCompare(b.name)}
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
|
contentFormatter={(item) => twoDecimalString(item.value) + ' GB'}
|
||||||
indicator="line"
|
indicator="line"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
cn,
|
cn,
|
||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
|
twoDecimalString,
|
||||||
useYaxisWidth,
|
useYaxisWidth,
|
||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
// import Spinner from '../spinner'
|
// import Spinner from '../spinner'
|
||||||
@@ -61,8 +62,8 @@ export default function SwapChart({
|
|||||||
animationDuration={150}
|
animationDuration={150}
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
unit=" GB"
|
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
|
contentFormatter={(item) => twoDecimalString(item.value) + ' GB'}
|
||||||
indicator="line"
|
indicator="line"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
cn,
|
cn,
|
||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
|
twoDecimalString,
|
||||||
useYaxisWidth,
|
useYaxisWidth,
|
||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
@@ -102,8 +103,8 @@ export default function TemperatureChart({
|
|||||||
itemSorter={(a, b) => b.value - a.value}
|
itemSorter={(a, b) => b.value - a.value}
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
unit=" °C"
|
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
|
contentFormatter={(item) => twoDecimalString(item.value) + ' °C'}
|
||||||
indicator="line"
|
indicator="line"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
49
beszel/site/src/components/copy-to-clipboard.tsx
Normal file
49
beszel/site/src/components/copy-to-clipboard.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { useEffect, useMemo, useRef } from 'react'
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog'
|
||||||
|
import { Textarea } from './ui/textarea'
|
||||||
|
import { $copyContent } from '@/lib/stores'
|
||||||
|
|
||||||
|
export default function CopyToClipboard({ content }: { content: string }) {
|
||||||
|
return (
|
||||||
|
<Dialog defaultOpen={true}>
|
||||||
|
<DialogContent className="w-[90%] rounded-lg" style={{ maxWidth: 530 }}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Could not copy to clipboard</DialogTitle>
|
||||||
|
<DialogDescription>Please copy the text manually.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<CopyTextarea content={content} />
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Clipboard API requires a secure context (https, localhost, or *.localhost)
|
||||||
|
</p>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CopyTextarea({ content }: { content: string }) {
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
|
||||||
|
const rows = useMemo(() => {
|
||||||
|
return content.split('\n').length
|
||||||
|
}, [content])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (textareaRef.current) {
|
||||||
|
textareaRef.current.select()
|
||||||
|
}
|
||||||
|
}, [textareaRef])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => $copyContent.set('')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
className="font-mono overflow-hidden whitespace-pre"
|
||||||
|
rows={rows}
|
||||||
|
value={content}
|
||||||
|
readOnly
|
||||||
|
ref={textareaRef}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MoonStarIcon, Sun } from 'lucide-react'
|
import { LaptopIcon, MoonStarIcon, SunIcon } from 'lucide-react'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
@@ -16,15 +16,24 @@ export function ModeToggle() {
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant={'ghost'} size="icon">
|
<Button variant={'ghost'} size="icon">
|
||||||
<Sun className="h-[1.2rem] w-[1.2rem] dark:opacity-0" />
|
<SunIcon className="h-[1.2rem] w-[1.2rem] dark:opacity-0" />
|
||||||
<MoonStarIcon className="absolute h-[1.2rem] w-[1.2rem] opacity-0 dark:opacity-100" />
|
<MoonStarIcon className="absolute h-[1.2rem] w-[1.2rem] opacity-0 dark:opacity-100" />
|
||||||
<span className="sr-only">Toggle theme</span>
|
<span className="sr-only">Toggle theme</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem onClick={() => setTheme('light')}>Light</DropdownMenuItem>
|
<DropdownMenuItem onClick={() => setTheme('light')}>
|
||||||
<DropdownMenuItem onClick={() => setTheme('dark')}>Dark</DropdownMenuItem>
|
<SunIcon className="mr-2.5 h-4 w-4" />
|
||||||
<DropdownMenuItem onClick={() => setTheme('system')}>System</DropdownMenuItem>
|
Light
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
||||||
|
<MoonStarIcon className="mr-2.5 h-4 w-4" />
|
||||||
|
Dark
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setTheme('system')}>
|
||||||
|
<LaptopIcon className="mr-2.5 h-4 w-4" />
|
||||||
|
System
|
||||||
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default function () {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Filter..."
|
placeholder="Filter..."
|
||||||
onChange={(e) => setFilter(e.target.value)}
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
className="w-full md:w-56 lg:w-80 ml-auto pl-4"
|
className="w-full md:w-56 lg:w-80 ml-auto px-4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -1,15 +1,30 @@
|
|||||||
import { $systems, pb, $chartTime } from '@/lib/stores'
|
import { $systems, pb, $chartTime, $containerFilter } from '@/lib/stores'
|
||||||
import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types'
|
import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types'
|
||||||
import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import Spinner from '../spinner'
|
import Spinner from '../spinner'
|
||||||
import { ClockArrowUp, CpuIcon, GlobeIcon } from 'lucide-react'
|
import {
|
||||||
|
ClockArrowUp,
|
||||||
|
CpuIcon,
|
||||||
|
GlobeIcon,
|
||||||
|
LayoutGridIcon,
|
||||||
|
StretchHorizontalIcon,
|
||||||
|
XIcon,
|
||||||
|
} from 'lucide-react'
|
||||||
import ChartTimeSelect from '../charts/chart-time-select'
|
import ChartTimeSelect from '../charts/chart-time-select'
|
||||||
import { chartTimeData, cn, getPbTimestamp, useClampedIsInViewport } from '@/lib/utils'
|
import {
|
||||||
|
chartTimeData,
|
||||||
|
cn,
|
||||||
|
getPbTimestamp,
|
||||||
|
useClampedIsInViewport,
|
||||||
|
useLocalStorage,
|
||||||
|
} from '@/lib/utils'
|
||||||
import { Separator } from '../ui/separator'
|
import { Separator } from '../ui/separator'
|
||||||
import { scaleTime } from 'd3-scale'
|
import { scaleTime } from 'd3-scale'
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
|
||||||
|
import { Button, buttonVariants } from '../ui/button'
|
||||||
|
import { Input } from '../ui/input'
|
||||||
|
|
||||||
const CpuChart = lazy(() => import('../charts/cpu-chart'))
|
const CpuChart = lazy(() => import('../charts/cpu-chart'))
|
||||||
const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart'))
|
const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart'))
|
||||||
@@ -21,14 +36,18 @@ const BandwidthChart = lazy(() => import('../charts/bandwidth-chart'))
|
|||||||
const ContainerNetChart = lazy(() => import('../charts/container-net-chart'))
|
const ContainerNetChart = lazy(() => import('../charts/container-net-chart'))
|
||||||
const SwapChart = lazy(() => import('../charts/swap-chart'))
|
const SwapChart = lazy(() => import('../charts/swap-chart'))
|
||||||
const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
|
const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
|
||||||
|
const ExFsDiskChart = lazy(() => import('../charts/extra-fs-disk-chart'))
|
||||||
|
const ExFsDiskIoChart = lazy(() => import('../charts/extra-fs-disk-io-chart'))
|
||||||
|
|
||||||
export default function SystemDetail({ name }: { name: string }) {
|
export default function SystemDetail({ name }: { name: string }) {
|
||||||
const systems = useStore($systems)
|
const systems = useStore($systems)
|
||||||
const chartTime = useStore($chartTime)
|
const chartTime = useStore($chartTime)
|
||||||
|
const [grid, setGrid] = useLocalStorage('grid', true)
|
||||||
const [ticks, setTicks] = useState([] as number[])
|
const [ticks, setTicks] = useState([] as number[])
|
||||||
const [system, setSystem] = useState({} as SystemRecord)
|
const [system, setSystem] = useState({} as SystemRecord)
|
||||||
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
|
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
|
||||||
const [hasDockerStats, setHasDocker] = useState(false)
|
const [hasDockerStats, setHasDocker] = useState(false)
|
||||||
|
const netCardRef = useRef<HTMLDivElement>(null)
|
||||||
const [dockerCpuChartData, setDockerCpuChartData] = useState<Record<string, number | string>[]>(
|
const [dockerCpuChartData, setDockerCpuChartData] = useState<Record<string, number | string>[]>(
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
@@ -44,6 +63,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
return () => {
|
return () => {
|
||||||
resetCharts()
|
resetCharts()
|
||||||
$chartTime.set('1h')
|
$chartTime.set('1h')
|
||||||
|
$containerFilter.set('')
|
||||||
setHasDocker(false)
|
setHasDocker(false)
|
||||||
}
|
}
|
||||||
}, [name])
|
}, [name])
|
||||||
@@ -105,7 +125,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
if (prevTime) {
|
if (prevTime) {
|
||||||
const interval = record.created - prevTime
|
const interval = record.created - prevTime
|
||||||
// if interval is too large, add a null record
|
// if interval is too large, add a null record
|
||||||
if (interval - interval * 0.5 > expectedInterval) {
|
if (interval > expectedInterval / 2 + expectedInterval) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
modifiedRecords.push({ created: null, stats: null })
|
modifiedRecords.push({ created: null, stats: null })
|
||||||
}
|
}
|
||||||
@@ -186,13 +206,28 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
return `${Math.trunc(system.info?.u / 86400)} days`
|
return `${Math.trunc(system.info?.u / 86400)} days`
|
||||||
}, [system.info?.u])
|
}, [system.info?.u])
|
||||||
|
|
||||||
|
/** Space for tooltip if more than 12 containers */
|
||||||
|
const bottomSpacing = useMemo(() => {
|
||||||
|
if (!netCardRef.current || !dockerNetChartData.length) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const tooltipHeight = (Object.keys(dockerNetChartData[0]).length - 11) * 17.8 - 40
|
||||||
|
const wrapperEl = document.getElementById('chartwrap') as HTMLDivElement
|
||||||
|
const wrapperRect = wrapperEl.getBoundingClientRect()
|
||||||
|
const chartRect = netCardRef.current.getBoundingClientRect()
|
||||||
|
const distanceToBottom = wrapperRect.bottom - chartRect.bottom
|
||||||
|
return tooltipHeight - distanceToBottom
|
||||||
|
}, [netCardRef.current, dockerNetChartData])
|
||||||
|
|
||||||
if (!system.id) {
|
if (!system.id) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid lg:grid-cols-2 gap-4 mb-10">
|
<>
|
||||||
<Card className="col-span-full">
|
<div id="chartwrap" className="grid gap-4 mb-10">
|
||||||
|
{/* system info */}
|
||||||
|
<Card>
|
||||||
<div className="grid lg:flex items-center gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
|
<div className="grid lg:flex items-center gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
|
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
|
||||||
@@ -217,8 +252,8 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
{system.status}
|
{system.status}
|
||||||
</div>
|
</div>
|
||||||
<Separator orientation="vertical" className="h-4 bg-primary/30" />
|
<Separator orientation="vertical" className="h-4 bg-primary/30" />
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5 items-center">
|
||||||
<GlobeIcon className="h-4 w-4 mt-[1px]" /> {system.host}
|
<GlobeIcon className="h-4 w-4" /> {system.host}
|
||||||
</div>
|
</div>
|
||||||
{system.info?.u && (
|
{system.info?.u && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@@ -244,72 +279,178 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChartTimeSelect className="w-full lg:w-40 xl:w-52 ml-auto max-sm:-mb-1" />
|
<div className="lg:ml-auto flex items-center gap-2 max-sm:-mb-1">
|
||||||
|
<ChartTimeSelect className="w-full lg:w-40" />
|
||||||
|
<TooltipProvider delayDuration={100}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
aria-label="Toggle grid"
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ variant: 'outline', size: 'icon' }),
|
||||||
|
'hidden lg:flex p-0 text-primary'
|
||||||
|
)}
|
||||||
|
onClick={() => setGrid(!grid)}
|
||||||
|
>
|
||||||
|
{grid ? (
|
||||||
|
<LayoutGridIcon className="h-[1.2rem] w-[1.2rem] opacity-85" />
|
||||||
|
) : (
|
||||||
|
<StretchHorizontalIcon className="h-[1.2rem] w-[1.2rem] opacity-85" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Toggle grid</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<ChartCard title="Total CPU Usage" description="Average system-wide CPU utilization">
|
{/* main charts */}
|
||||||
|
<div className="grid lg:grid-cols-2 gap-4">
|
||||||
|
<ChartCard
|
||||||
|
grid={grid}
|
||||||
|
title="Total CPU Usage"
|
||||||
|
description="Average system-wide CPU utilization"
|
||||||
|
>
|
||||||
<CpuChart ticks={ticks} systemData={systemStats} />
|
<CpuChart ticks={ticks} systemData={systemStats} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{hasDockerStats && (
|
{hasDockerStats && (
|
||||||
<ChartCard title="Docker CPU Usage" description="CPU utilization of docker containers">
|
<ChartCard
|
||||||
|
grid={grid}
|
||||||
|
title="Docker CPU Usage"
|
||||||
|
description="CPU utilization of docker containers"
|
||||||
|
isContainerChart={true}
|
||||||
|
>
|
||||||
<ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} />
|
<ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ChartCard title="Total Memory Usage" description="Precise utilization at the recorded time">
|
<ChartCard
|
||||||
|
grid={grid}
|
||||||
|
title="Total Memory Usage"
|
||||||
|
description="Precise utilization at the recorded time"
|
||||||
|
>
|
||||||
<MemChart ticks={ticks} systemData={systemStats} />
|
<MemChart ticks={ticks} systemData={systemStats} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{hasDockerStats && (
|
{hasDockerStats && (
|
||||||
<ChartCard title="Docker Memory Usage" description="Memory usage of docker containers">
|
<ChartCard
|
||||||
|
grid={grid}
|
||||||
|
title="Docker Memory Usage"
|
||||||
|
description="Memory usage of docker containers"
|
||||||
|
isContainerChart={true}
|
||||||
|
>
|
||||||
<ContainerMemChart chartData={dockerMemChartData} ticks={ticks} />
|
<ContainerMemChart chartData={dockerMemChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(systemStats.at(-1)?.stats.s ?? 0) > 0 && (
|
{(systemStats.at(-1)?.stats.s ?? 0) > 0 && (
|
||||||
<ChartCard title="Swap Usage" description="Swap space used by the system">
|
<ChartCard grid={grid} title="Swap Usage" description="Swap space used by the system">
|
||||||
<SwapChart ticks={ticks} systemData={systemStats} />
|
<SwapChart ticks={ticks} systemData={systemStats} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{systemStats.at(-1)?.stats.t && (
|
<ChartCard grid={grid} title="Disk Space" description="Usage of root partition">
|
||||||
<ChartCard title="Temperature" description="Temperatures of system sensors">
|
|
||||||
<TemperatureChart ticks={ticks} systemData={systemStats} />
|
|
||||||
</ChartCard>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ChartCard title="Disk Usage" description="Space usage of root partition">
|
|
||||||
<DiskChart ticks={ticks} systemData={systemStats} />
|
<DiskChart ticks={ticks} systemData={systemStats} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
<ChartCard title="Disk I/O" description="Throughput of root filesystem">
|
<ChartCard grid={grid} title="Disk I/O" description="Throughput of root filesystem">
|
||||||
<DiskIoChart ticks={ticks} systemData={systemStats} />
|
<DiskIoChart ticks={ticks} systemData={systemStats} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
<ChartCard title="Bandwidth" description="Network traffic of public interfaces">
|
<ChartCard
|
||||||
|
grid={grid}
|
||||||
|
title="Bandwidth"
|
||||||
|
description="Network traffic of public interfaces"
|
||||||
|
>
|
||||||
<BandwidthChart ticks={ticks} systemData={systemStats} />
|
<BandwidthChart ticks={ticks} systemData={systemStats} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{hasDockerStats && dockerNetChartData.length > 0 && (
|
{hasDockerStats && dockerNetChartData.length > 0 && (
|
||||||
<>
|
<div
|
||||||
|
ref={netCardRef}
|
||||||
|
className={cn({
|
||||||
|
'col-span-full': !grid,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<ChartCard
|
<ChartCard
|
||||||
title="Docker Network I/O"
|
title="Docker Network I/O"
|
||||||
description="Includes traffic between internal services"
|
description="Includes traffic between internal services"
|
||||||
|
isContainerChart={true}
|
||||||
>
|
>
|
||||||
<ContainerNetChart chartData={dockerNetChartData} ticks={ticks} />
|
<ContainerNetChart chartData={dockerNetChartData} ticks={ticks} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
{/* add space for tooltip if more than 12 containers */}
|
</div>
|
||||||
{Object.keys(dockerNetChartData[0]).length > 12 && (
|
|
||||||
<span
|
|
||||||
className="block"
|
|
||||||
style={{
|
|
||||||
height: (Object.keys(dockerNetChartData[0]).length - 13) * 18,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{systemStats.at(-1)?.stats.t && (
|
||||||
|
<ChartCard grid={grid} title="Temperature" description="Temperatures of system sensors">
|
||||||
|
<TemperatureChart ticks={ticks} systemData={systemStats} />
|
||||||
|
</ChartCard>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* extra filesystem charts */}
|
||||||
|
{Object.keys(systemStats.at(-1)?.stats.efs ?? {}).length > 0 && (
|
||||||
|
<div className="grid lg:grid-cols-2 gap-4">
|
||||||
|
{Object.keys(systemStats.at(-1)?.stats.efs ?? {}).map((extraFsName) => {
|
||||||
|
return (
|
||||||
|
<div key={extraFsName} className="contents">
|
||||||
|
<ChartCard
|
||||||
|
grid={true}
|
||||||
|
title={`${extraFsName} Usage`}
|
||||||
|
description={`Disk usage of ${extraFsName}`}
|
||||||
|
>
|
||||||
|
<ExFsDiskChart ticks={ticks} systemData={systemStats} fs={extraFsName} />
|
||||||
|
</ChartCard>
|
||||||
|
<ChartCard
|
||||||
|
grid={true}
|
||||||
|
title={`${extraFsName} I/O`}
|
||||||
|
description={`Throughput of of ${extraFsName}`}
|
||||||
|
>
|
||||||
|
<ExFsDiskIoChart ticks={ticks} systemData={systemStats} fs={extraFsName} />
|
||||||
|
</ChartCard>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* add space for tooltip if more than 12 containers */}
|
||||||
|
{bottomSpacing > 0 && <span className="block" style={{ height: bottomSpacing }} />}
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContainerFilterBar() {
|
||||||
|
const containerFilter = useStore($containerFilter)
|
||||||
|
|
||||||
|
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
$containerFilter.set(e.target.value)
|
||||||
|
}, []) // Use an empty dependency array to prevent re-creation
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:right-3.5">
|
||||||
|
<Input
|
||||||
|
placeholder="Filter..."
|
||||||
|
className="pl-4 pr-8"
|
||||||
|
value={containerFilter}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
{containerFilter && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
aria-label="Clear"
|
||||||
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
|
||||||
|
onClick={() => $containerFilter.set('')}
|
||||||
|
>
|
||||||
|
<XIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -319,21 +460,27 @@ function ChartCard({
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
children,
|
children,
|
||||||
|
grid,
|
||||||
|
isContainerChart,
|
||||||
}: {
|
}: {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
grid?: boolean
|
||||||
|
isContainerChart?: boolean
|
||||||
}) {
|
}) {
|
||||||
const target = useRef<HTMLDivElement>(null)
|
const target = useRef<HTMLDivElement>(null)
|
||||||
const [isInViewport, wrappedTargetRef] = useClampedIsInViewport({ target: target })
|
const [isInViewport, wrappedTargetRef] = useClampedIsInViewport({ target: target })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="pb-2 sm:pb-4 even:last-of-type:col-span-full" ref={wrappedTargetRef}>
|
<Card
|
||||||
|
className={cn('pb-2 sm:pb-4 odd:last-of-type:col-span-full', { 'col-span-full': !grid })}
|
||||||
|
ref={wrappedTargetRef}
|
||||||
|
>
|
||||||
<CardHeader className="pb-5 pt-4 relative space-y-1 max-sm:py-3 max-sm:px-4">
|
<CardHeader className="pb-5 pt-4 relative space-y-1 max-sm:py-3 max-sm:px-4">
|
||||||
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
|
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
|
||||||
<CardDescription>{description}</CardDescription>
|
<CardDescription>{description}</CardDescription>
|
||||||
{/* <div className="w-full pt-1 sm:w-40 hidden sm:block absolute top-1.5 right-3.5">
|
{isContainerChart && <ContainerFilterBar />}
|
||||||
<ChartTimeSelect />
|
|
||||||
</div> */}
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
|
<CardContent className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
|
||||||
{<Spinner />}
|
{<Spinner />}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute inset-0 w-full h-full origin-left',
|
'absolute inset-0 w-full h-full origin-left',
|
||||||
(val < 60 && 'bg-green-500') || (val < 90 && 'bg-yellow-500') || 'bg-red-600'
|
(val < 65 && 'bg-green-500') || (val < 90 && 'bg-yellow-500') || 'bg-red-600'
|
||||||
)}
|
)}
|
||||||
style={{ transform: `scalex(${val}%)` }}
|
style={{ transform: `scalex(${val}%)` }}
|
||||||
></span>
|
></span>
|
||||||
@@ -135,7 +135,7 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
|||||||
<Button
|
<Button
|
||||||
data-nolink
|
data-nolink
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
className="text-foreground/90 h-7 px-1.5 gap-1.5"
|
className="text-primary/90 h-7 px-1.5 gap-1.5"
|
||||||
onClick={() => copyToClipboard(info.getValue() as string)}
|
onClick={() => copyToClipboard(info.getValue() as string)}
|
||||||
>
|
>
|
||||||
{info.getValue() as string}
|
{info.getValue() as string}
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
nameKey?: string
|
nameKey?: string
|
||||||
labelKey?: string
|
labelKey?: string
|
||||||
unit?: string
|
unit?: string
|
||||||
|
filter?: string
|
||||||
contentFormatter?: (item: any, key: string) => React.ReactNode | string
|
contentFormatter?: (item: any, key: string) => React.ReactNode | string
|
||||||
}
|
}
|
||||||
>(
|
>(
|
||||||
@@ -119,6 +120,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
nameKey,
|
nameKey,
|
||||||
labelKey,
|
labelKey,
|
||||||
unit,
|
unit,
|
||||||
|
filter,
|
||||||
itemSorter,
|
itemSorter,
|
||||||
contentFormatter: content = undefined,
|
contentFormatter: content = undefined,
|
||||||
},
|
},
|
||||||
@@ -127,6 +129,9 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
const { config } = useChart()
|
const { config } = useChart()
|
||||||
|
|
||||||
React.useMemo(() => {
|
React.useMemo(() => {
|
||||||
|
if (filter) {
|
||||||
|
payload = payload?.filter((item) => (item.name as string)?.includes(filter))
|
||||||
|
}
|
||||||
if (itemSorter) {
|
if (itemSorter) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
payload?.sort(itemSorter)
|
payload?.sort(itemSorter)
|
||||||
@@ -229,7 +234,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{item.value !== undefined && (
|
{item.value !== undefined && (
|
||||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
<span className="font-medium tabular-nums text-foreground">
|
||||||
{content && typeof content === 'function'
|
{content && typeof content === 'function'
|
||||||
? content(item, key)
|
? content(item, key)
|
||||||
: item.value.toLocaleString() + (unit ? unit : '')}
|
: item.value.toLocaleString() + (unit ? unit : '')}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const Command = React.forwardRef<
|
|||||||
<CommandPrimitive
|
<CommandPrimitive
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
|
'flex h-full w-full flex-col overflow-hidden bg-popover text-popover-foreground',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,40 +1,31 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const Table = React.forwardRef<
|
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
||||||
HTMLTableElement,
|
({ className, ...props }, ref) => (
|
||||||
React.HTMLAttributes<HTMLTableElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div className="relative w-full overflow-auto">
|
<div className="relative w-full overflow-auto">
|
||||||
<table
|
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn("w-full caption-bottom text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
))
|
)
|
||||||
Table.displayName = "Table"
|
)
|
||||||
|
Table.displayName = 'Table'
|
||||||
|
|
||||||
const TableHeader = React.forwardRef<
|
const TableHeader = React.forwardRef<
|
||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
|
||||||
))
|
))
|
||||||
TableHeader.displayName = "TableHeader"
|
TableHeader.displayName = 'TableHeader'
|
||||||
|
|
||||||
const TableBody = React.forwardRef<
|
const TableBody = React.forwardRef<
|
||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<tbody
|
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn("[&_tr:last-child]:border-0", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
))
|
||||||
TableBody.displayName = "TableBody"
|
TableBody.displayName = 'TableBody'
|
||||||
|
|
||||||
const TableFooter = React.forwardRef<
|
const TableFooter = React.forwardRef<
|
||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
@@ -42,29 +33,25 @@ const TableFooter = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<tfoot
|
<tfoot
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)}
|
||||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
TableFooter.displayName = "TableFooter"
|
TableFooter.displayName = 'TableFooter'
|
||||||
|
|
||||||
const TableRow = React.forwardRef<
|
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||||
HTMLTableRowElement,
|
({ className, ...props }, ref) => (
|
||||||
React.HTMLAttributes<HTMLTableRowElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<tr
|
<tr
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
'border-b transition-colors hover:bg-muted/40 dark:hover:bg-muted/30 data-[state=selected]:bg-muted',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
TableRow.displayName = "TableRow"
|
)
|
||||||
|
TableRow.displayName = 'TableRow'
|
||||||
|
|
||||||
const TableHead = React.forwardRef<
|
const TableHead = React.forwardRef<
|
||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
@@ -73,13 +60,13 @@ const TableHead = React.forwardRef<
|
|||||||
<th
|
<th
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
TableHead.displayName = "TableHead"
|
TableHead.displayName = 'TableHead'
|
||||||
|
|
||||||
const TableCell = React.forwardRef<
|
const TableCell = React.forwardRef<
|
||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
@@ -87,31 +74,18 @@ const TableCell = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<td
|
<td
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
TableCell.displayName = "TableCell"
|
TableCell.displayName = 'TableCell'
|
||||||
|
|
||||||
const TableCaption = React.forwardRef<
|
const TableCaption = React.forwardRef<
|
||||||
HTMLTableCaptionElement,
|
HTMLTableCaptionElement,
|
||||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<caption
|
<caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
))
|
||||||
TableCaption.displayName = "TableCaption"
|
TableCaption.displayName = 'TableCaption'
|
||||||
|
|
||||||
export {
|
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
|
||||||
Table,
|
|
||||||
TableHeader,
|
|
||||||
TableBody,
|
|
||||||
TableFooter,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
TableCell,
|
|
||||||
TableCaption,
|
|
||||||
}
|
|
||||||
|
|||||||
23
beszel/site/src/components/ui/textarea.tsx
Normal file
23
beszel/site/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={cn(
|
||||||
|
'flex min-h-14 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Textarea.displayName = 'Textarea'
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 30 8% 98.5%;
|
--background: 30 8% 98.5%;
|
||||||
@@ -23,9 +24,16 @@
|
|||||||
--input: 30 4.29% 72.55%;
|
--input: 30 4.29% 72.55%;
|
||||||
--ring: 30 3.97% 49.41%;
|
--ring: 30 3.97% 49.41%;
|
||||||
--radius: 0.8rem;
|
--radius: 0.8rem;
|
||||||
|
/* charts */
|
||||||
|
--chart-1: 220 70% 50%;
|
||||||
|
--chart-2: 160 60% 45%;
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 340 75% 55%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
|
color-scheme: dark;
|
||||||
--background: 240 10% 6.2%;
|
--background: 240 10% 6.2%;
|
||||||
--foreground: 0 0% 98.04%;
|
--foreground: 0 0% 98.04%;
|
||||||
--card: 240 8.57% 8%;
|
--card: 240 8.57% 8%;
|
||||||
@@ -49,6 +57,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fonts */
|
||||||
|
@supports (font-variation-settings: normal) {
|
||||||
|
:root {
|
||||||
|
font-family: Inter, InterVariable, sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: InterVariable;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/static/InterVariable.woff2?v=4.0') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
@@ -65,27 +87,3 @@
|
|||||||
.recharts-yAxis {
|
.recharts-yAxis {
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* charts */
|
|
||||||
@layer base {
|
|
||||||
:root {
|
|
||||||
/* --chart-1: 12 76% 61%;
|
|
||||||
--chart-2: 173 58% 39%;
|
|
||||||
--chart-3: 197 37% 24%;
|
|
||||||
--chart-4: 43 74% 66%;
|
|
||||||
--chart-5: 27 87% 67%; */
|
|
||||||
--chart-1: 220 70% 50%;
|
|
||||||
--chart-2: 160 60% 45%;
|
|
||||||
--chart-3: 30 80% 55%;
|
|
||||||
--chart-4: 280 65% 60%;
|
|
||||||
--chart-5: 340 75% 55%;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
.dark {
|
|
||||||
--chart-1: 220 70% 50%;
|
|
||||||
--chart-2: 160 60% 45%;
|
|
||||||
--chart-3: 30 80% 55%;
|
|
||||||
--chart-4: 280 65% 60%;
|
|
||||||
--chart-5: 340 75% 55%;
|
|
||||||
} */
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -22,3 +22,9 @@ export const $hubVersion = atom('')
|
|||||||
|
|
||||||
/** Chart time period */
|
/** Chart time period */
|
||||||
export const $chartTime = atom('1h') as WritableAtom<ChartTimes>
|
export const $chartTime = atom('1h') as WritableAtom<ChartTimes>
|
||||||
|
|
||||||
|
/** Container chart filter */
|
||||||
|
export const $containerFilter = atom('')
|
||||||
|
|
||||||
|
/** Fallback copy to clipboard dialog content */
|
||||||
|
export const $copyContent = atom('')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { toast } from '@/components/ui/use-toast'
|
import { toast } from '@/components/ui/use-toast'
|
||||||
import { type ClassValue, clsx } from 'clsx'
|
import { type ClassValue, clsx } from 'clsx'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
import { $alerts, $systems, pb } from './stores'
|
import { $alerts, $copyContent, $systems, pb } from './stores'
|
||||||
import { AlertRecord, ChartTimeData, ChartTimes, SystemRecord } from '@/types'
|
import { AlertRecord, ChartTimeData, ChartTimes, SystemRecord } from '@/types'
|
||||||
import { RecordModel, RecordSubscription } from 'pocketbase'
|
import { RecordModel, RecordSubscription } from 'pocketbase'
|
||||||
import { WritableAtom } from 'nanostores'
|
import { WritableAtom } from 'nanostores'
|
||||||
@@ -22,10 +22,7 @@ export async function copyToClipboard(content: string) {
|
|||||||
description: 'Copied to clipboard',
|
description: 'Copied to clipboard',
|
||||||
})
|
})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
prompt(
|
$copyContent.set(content)
|
||||||
'Automatic copy requires a secure context (https, localhost, or *.localhost). Please copy manually:',
|
|
||||||
content
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,3 +238,35 @@ export function toFixedWithoutTrailingZeros(num: number, digits: number) {
|
|||||||
export function toFixedFloat(num: number, digits: number) {
|
export function toFixedFloat(num: number, digits: number) {
|
||||||
return parseFloat(num.toFixed(digits))
|
return parseFloat(num.toFixed(digits))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let twoDecimalFormatter: Intl.NumberFormat
|
||||||
|
/** Format number to two decimal places */
|
||||||
|
export function twoDecimalString(num: number) {
|
||||||
|
if (!twoDecimalFormatter) {
|
||||||
|
twoDecimalFormatter = new Intl.NumberFormat(undefined, {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Return a function that formats numbers using the saved formatter
|
||||||
|
return twoDecimalFormatter.format(num)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get value from local storage */
|
||||||
|
function getStorageValue(key: string, defaultValue: any) {
|
||||||
|
const saved = localStorage?.getItem(key)
|
||||||
|
return saved ? JSON.parse(saved) : defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hook to sync value in local storage */
|
||||||
|
export const useLocalStorage = (key: string, defaultValue: any) => {
|
||||||
|
key = `besz-${key}`
|
||||||
|
const [value, setValue] = useState(() => {
|
||||||
|
return getStorageValue(key, defaultValue)
|
||||||
|
})
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage?.setItem(key, JSON.stringify(value))
|
||||||
|
}, [key, value])
|
||||||
|
|
||||||
|
return [value, setValue]
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,9 +3,23 @@ import React, { Suspense, lazy, useEffect } from 'react'
|
|||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import Home from './components/routes/home.tsx'
|
import Home from './components/routes/home.tsx'
|
||||||
import { ThemeProvider } from './components/theme-provider.tsx'
|
import { ThemeProvider } from './components/theme-provider.tsx'
|
||||||
import { $authenticated, $systems, pb, $publicKey, $hubVersion } from './lib/stores.ts'
|
import {
|
||||||
|
$authenticated,
|
||||||
|
$systems,
|
||||||
|
pb,
|
||||||
|
$publicKey,
|
||||||
|
$hubVersion,
|
||||||
|
$copyContent,
|
||||||
|
} from './lib/stores.ts'
|
||||||
import { ModeToggle } from './components/mode-toggle.tsx'
|
import { ModeToggle } from './components/mode-toggle.tsx'
|
||||||
import { cn, isAdmin, updateAlerts, updateFavicon, updateSystemList } from './lib/utils.ts'
|
import {
|
||||||
|
cn,
|
||||||
|
isAdmin,
|
||||||
|
isReadOnlyUser,
|
||||||
|
updateAlerts,
|
||||||
|
updateFavicon,
|
||||||
|
updateSystemList,
|
||||||
|
} from './lib/utils.ts'
|
||||||
import { buttonVariants } from './components/ui/button.tsx'
|
import { buttonVariants } from './components/ui/button.tsx'
|
||||||
import {
|
import {
|
||||||
DatabaseBackupIcon,
|
DatabaseBackupIcon,
|
||||||
@@ -35,6 +49,7 @@ import { AddSystemButton } from './components/add-system.tsx'
|
|||||||
// const ServerDetail = lazy(() => import('./components/routes/system.tsx'))
|
// const ServerDetail = lazy(() => import('./components/routes/system.tsx'))
|
||||||
const CommandPalette = lazy(() => import('./components/command-palette.tsx'))
|
const CommandPalette = lazy(() => import('./components/command-palette.tsx'))
|
||||||
const LoginPage = lazy(() => import('./components/login/login.tsx'))
|
const LoginPage = lazy(() => import('./components/login/login.tsx'))
|
||||||
|
const CopyToClipboardDialog = lazy(() => import('./components/copy-to-clipboard.tsx'))
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const page = useStore($router)
|
const page = useStore($router)
|
||||||
@@ -89,6 +104,7 @@ const App = () => {
|
|||||||
|
|
||||||
const Layout = () => {
|
const Layout = () => {
|
||||||
const authenticated = useStore($authenticated)
|
const authenticated = useStore($authenticated)
|
||||||
|
const copyContent = useStore($copyContent)
|
||||||
|
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
return (
|
return (
|
||||||
@@ -125,7 +141,7 @@ const Layout = () => {
|
|||||||
<UserIcon className="h-[1.2rem] w-[1.2rem]" />
|
<UserIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="min-w-44">
|
<DropdownMenuContent align={isReadOnlyUser() ? 'end' : 'center'} className="min-w-44">
|
||||||
<DropdownMenuLabel>{pb.authStore.model?.email}</DropdownMenuLabel>
|
<DropdownMenuLabel>{pb.authStore.model?.email}</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
@@ -180,16 +196,23 @@ const Layout = () => {
|
|||||||
<Suspense>
|
<Suspense>
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
{copyContent && (
|
||||||
|
<Suspense>
|
||||||
|
<CopyToClipboardDialog content={copyContent} />
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('app')!).render(
|
ReactDOM.createRoot(document.getElementById('app')!).render(
|
||||||
<React.StrictMode>
|
// strict mode in dev mounts / unmounts components twice
|
||||||
|
// and breaks the clipboard dialog
|
||||||
|
//<React.StrictMode>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<Layout />
|
<Layout />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</React.StrictMode>
|
//</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
|||||||
13
beszel/site/src/types.d.ts
vendored
13
beszel/site/src/types.d.ts
vendored
@@ -59,6 +59,19 @@ export interface SystemStats {
|
|||||||
nr: number
|
nr: number
|
||||||
/** temperatures */
|
/** temperatures */
|
||||||
t?: Record<string, number>
|
t?: Record<string, number>
|
||||||
|
/** extra filesystems */
|
||||||
|
efs?: Record<string, ExtraFsStats>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtraFsStats {
|
||||||
|
/** disk size (gb) */
|
||||||
|
d: number
|
||||||
|
/** disk used (gb) */
|
||||||
|
du: number
|
||||||
|
/** total read (mb) */
|
||||||
|
r: number
|
||||||
|
/** total write (mb) */
|
||||||
|
w: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContainerStatsRecord extends RecordModel {
|
export interface ContainerStatsRecord extends RecordModel {
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ module.exports = {
|
|||||||
'2xl': '1400px',
|
'2xl': '1400px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter', 'sans-serif'],
|
sans: 'Inter, sans-serif',
|
||||||
// body: ['Inter', 'sans-serif'],
|
// body: ['Inter', 'sans-serif'],
|
||||||
// display: ['Inter', 'sans-serif'],
|
// display: ['Inter', 'sans-serif'],
|
||||||
},
|
},
|
||||||
extend: {
|
|
||||||
screens: {
|
screens: {
|
||||||
xs: '425px',
|
xs: '425px',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package beszel
|
package beszel
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version = "0.2.0"
|
Version = "0.3.0"
|
||||||
AppName = "beszel"
|
AppName = "beszel"
|
||||||
)
|
)
|
||||||
|
|||||||
53
readme.md
53
readme.md
@@ -57,7 +57,7 @@ The agent uses the host network mode so it can access network interface stats. T
|
|||||||
|
|
||||||
If you don't need network stats, remove that line from the compose file and map the port manually.
|
If you don't need network stats, remove that line from the compose file and map the port manually.
|
||||||
|
|
||||||
> **Note**: The docker version of the agent cannot automatically detect the filesystem to use for disk I/O stats, so include the `FILESYSTEM` environment variable if you want that to work ([instructions here](#finding-the-correct-filesystem)).
|
> **Note**: If disk I/O stats are missing or incorrect, try using the `FILESYSTEM` environment variable ([instructions here](#finding-the-correct-filesystem)). Check agent logs to see the current device being used.
|
||||||
|
|
||||||
### Binary
|
### Binary
|
||||||
|
|
||||||
@@ -105,15 +105,16 @@ Use `./beszel update` and `./beszel-agent update` to update to the latest versio
|
|||||||
### Agent
|
### Agent
|
||||||
|
|
||||||
| Name | Default | Description |
|
| Name | Default | Description |
|
||||||
| ------------- | ------- | ------------------------------------------------------------------ |
|
| ------------------- | ------- | ---------------------------------------------------------------------------------------- |
|
||||||
| `DOCKER_HOST` | unset | Overrides the docker host (docker.sock) if using a proxy.[^socket] |
|
| `DOCKER_HOST` | unset | Overrides the docker host (docker.sock) if using a proxy.[^socket] |
|
||||||
| `FILESYSTEM` | unset | Filesystem / partition to use for disk I/O stats. |
|
| `EXTRA_FILESYSTEMS` | unset | See [Monitoring additional disks / partitions](#monitoring-additional-disks--partitions) |
|
||||||
|
| `FILESYSTEM` | unset | Device or partition to use for root disk I/O stats. |
|
||||||
| `KEY` | unset | Public SSH key to use for authentication. Provided in hub. |
|
| `KEY` | unset | Public SSH key to use for authentication. Provided in hub. |
|
||||||
| `PORT` | 45876 | Port or address:port to listen on. |
|
| `PORT` | 45876 | Port or address:port to listen on. |
|
||||||
|
|
||||||
[^socket]: Beszel only needs access to read container information. For [linuxserver/docker-socket-proxy](https://github.com/linuxserver/docker-socket-proxy) you would set `CONTAINERS=1`.
|
[^socket]: Beszel only needs access to read container information. For [linuxserver/docker-socket-proxy](https://github.com/linuxserver/docker-socket-proxy) you would set `CONTAINERS=1`.
|
||||||
|
|
||||||
## OAuth / OIDC setup
|
## OAuth / OIDC Setup
|
||||||
|
|
||||||
Beszel supports OpenID Connect and many OAuth2 authentication providers (see list below).
|
Beszel supports OpenID Connect and many OAuth2 authentication providers (see list below).
|
||||||
|
|
||||||
@@ -146,6 +147,33 @@ Visit the "Auth providers" page to enable your provider. The redirect / callback
|
|||||||
- Yandex
|
- Yandex
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
## Monitoring additional disks / partitions
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> This feature is new and has been tested on a limited number of systems. Please report any issues.
|
||||||
|
|
||||||
|
You can configure the agent to monitor the usage and I/O of more than one disk or partition. The approach differs depending on the deployment method.
|
||||||
|
|
||||||
|
Use `lsblk` to find the names and mount points of your partitions. If you have trouble, check the agent logs.
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
Mount a folder from the partition's filesystem in the container's `/extra-filesystems` directory, like the example below. The charts will use the name of the device or partition, not the name of the folder.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- /mnt/disk1/.beszel:/extra-filesystems/disk1:ro
|
||||||
|
- /dev/mmcblk0/.beszel:/extra-filesystems/sd-card:ro
|
||||||
|
```
|
||||||
|
|
||||||
|
### Binary
|
||||||
|
|
||||||
|
Set the `EXTRA_FILESYSTEMS` environment variable to a comma-separated list of devices or partitions to monitor. For example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
EXTRA_FILESYSTEMS="sdb,sdc1,mmcblk0"
|
||||||
|
```
|
||||||
|
|
||||||
## REST API
|
## REST API
|
||||||
|
|
||||||
Because Beszel is built on PocketBase, you can use the PocketBase [web APIs](https://pocketbase.io/docs/api-records/) and [client-side SDKs](https://pocketbase.io/docs/client-side-sdks/) to read or update data from outside Beszel itself.
|
Because Beszel is built on PocketBase, you can use the PocketBase [web APIs](https://pocketbase.io/docs/api-records/) and [client-side SDKs](https://pocketbase.io/docs/client-side-sdks/) to read or update data from outside Beszel itself.
|
||||||
@@ -197,14 +225,20 @@ Otherwise you can use the agent's `container_name` as the hostname if both are i
|
|||||||
|
|
||||||
### Finding the correct filesystem
|
### Finding the correct filesystem
|
||||||
|
|
||||||
The filesystem / partition to use for disk I/O stats is specified in the `FILESYSTEM` environment variable.
|
The filesystem / device / partition to use for disk I/O stats is specified in the `FILESYSTEM` environment variable.
|
||||||
|
|
||||||
If it's not set, the agent will try to find the filesystem mounted on `/` and use that. This doesn't seem to work in a container, so it's recommended to set this value. One of the following methods should work (you usually want the option mounted on `/`):
|
If it's not set, the agent will try to find the partition mounted on `/` and use that. This doesn't seem to work in a container, so it's recommended to set this value. One of the following methods should work (you usually want the option mounted on `/`):
|
||||||
|
|
||||||
- Run `df -h` and choose an option under "Filesystem"
|
|
||||||
- Run `lsblk` and choose an option under "NAME"
|
- Run `lsblk` and choose an option under "NAME"
|
||||||
|
- Run `df -h` and choose an option under "Filesystem"
|
||||||
- Run `sudo fdisk -l` and choose an option under "Device"
|
- Run `sudo fdisk -l` and choose an option under "Device"
|
||||||
|
|
||||||
|
### Docker container charts are empty or missing
|
||||||
|
|
||||||
|
If container charts show empty data, or don't show up at all, you may need to enable cgroup memory accounting. To verify, run `docker stats`. If that also shows zero memory usage, follow this guide to fix:
|
||||||
|
|
||||||
|
https://akashrajpurohit.com/blog/resolving-missing-memory-stats-in-docker-stats-on-raspberry-pi/
|
||||||
|
|
||||||
### Docker containers are not populating reliably
|
### Docker containers are not populating reliably
|
||||||
|
|
||||||
Try upgrading your docker version on the agent system. I had this issue on a machine running version 24. It was fixed by upgrading to version 27.
|
Try upgrading your docker version on the agent system. I had this issue on a machine running version 24. It was fixed by upgrading to version 27.
|
||||||
@@ -260,8 +294,3 @@ GOOS=freebsd GOARCH=arm64 CGO_ENABLED=0 go build -ldflags "-w -s" .
|
|||||||
```
|
```
|
||||||
|
|
||||||
You can see a list of valid options by running `go tool dist list`.
|
You can see a list of valid options by running `go tool dist list`.
|
||||||
|
|
||||||
<!--
|
|
||||||
## Support
|
|
||||||
|
|
||||||
My country, the USA, and many others, are actively involved in the genocide of the Palestinian people. I would greatly appreciate any effort you could make to pressure your government to stop enabling this violence. -->
|
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ services:
|
|||||||
network_mode: host
|
network_mode: host
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
# monitor other disks / partitions by mounting a folder in /extra-filesystems
|
||||||
|
# - /mnt/disk1/.beszel:/extra-filesystems/disk1:ro
|
||||||
environment:
|
environment:
|
||||||
PORT: 45876
|
PORT: 45876
|
||||||
KEY: 'ssh-ed25519 YOUR_PUBLIC_KEY'
|
KEY: 'ssh-ed25519 YOUR_PUBLIC_KEY'
|
||||||
# FILESYSTEM: /dev/sda1 # set to the correct filesystem for disk I/O stats
|
# FILESYSTEM: /dev/sda1 # override the root partition / device for disk I/O stats
|
||||||
|
|||||||
Reference in New Issue
Block a user