Compare commits

...

19 Commits

Author SHA1 Message Date
Henry Dollman
8de2dee4e9 built-in agent 2024-10-07 18:58:57 -04:00
Henry Dollman
7a82571921 add time back to memory chart tooltip 2024-10-07 18:19:28 -04:00
Henry Dollman
e81f8ac387 add zfs arc to longer records 2024-10-07 11:45:09 -04:00
Henry Dollman
05faa88e6a release 0.5.2 2024-10-05 18:30:10 -04:00
Henry Dollman
73aae62c2e add ZFS ARC memory accounting 2024-10-05 18:07:42 -04:00
Henry Dollman
af4877ca30 add MEM_CALC env var 2024-10-05 15:29:27 -04:00
Henry Dollman
c407fe9af0 exclude sensor if temp <=0 || temp >= 200 2024-10-05 11:14:20 -04:00
hank
13c9497951 Merge pull request #199 from Bot-wxt1221/main
fix(package-lock.json): fix for nixpkgs
2024-10-04 11:06:58 -04:00
wxt
4274096645 fix(package-lock.json): fix for nixpkgs 2024-10-03 16:18:31 +08:00
Henry Dollman
a213b70a1c release 0.5.1 2024-10-02 19:58:44 -04:00
Henry Dollman
66cc0a4b24 log stats on startup if log level is debug 2024-10-02 19:58:02 -04:00
Henry Dollman
f051f6a5f8 add dockerManager / fix for Docker 24 and older
* dockerManager now handles all docker api interaction and container metrics tracking
* sets unlimited concurrency for docker 24 and older
2024-10-02 19:45:26 -04:00
Henry Dollman
b9f142c28c update go deps (gopsutil v4.24.9) 2024-10-02 19:11:13 -04:00
Henry Dollman
45e1283b83 move system.Info to Agent struct
* cleaner to store entire info struct rather than separate properties for unchanging values
2024-10-02 12:34:42 -04:00
Henry Dollman
94cb5f2798 fix uptime hours pluralization 2024-10-02 12:33:31 -04:00
Henry Dollman
2883467b2b update docker image workflow
- change tag to "*v"
- remove cache
2024-10-02 12:32:19 -04:00
Lindemberg Barbosa
0c77190161 fix: readme link for Monitoring additional disks, partitions, or remote mounts (#195) 2024-10-02 10:40:12 -04:00
Henry Dollman
8d4d072343 fix readme backticks for telnet command 2024-09-30 15:45:46 -04:00
Henry Dollman
d6e0daf52a update js deps and add package-lock.json (#192)
- replaces use-is-in-viewport package with lib/use-intersection-observer.ts due to npm dependency conflict
2024-09-30 14:37:59 -04:00
23 changed files with 5452 additions and 251 deletions

View File

@@ -3,7 +3,7 @@ name: Make docker images
on: on:
push: push:
tags: tags:
- '*' - 'v*'
jobs: jobs:
build: build:
@@ -71,5 +71,3 @@ jobs:
push: ${{ github.ref_type == 'tag' }} push: ${{ github.ref_type == 'tag' }}
tags: ${{ steps.metadata.outputs.tags }} tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }} labels: ${{ steps.metadata.outputs.labels }}
cache-from: type=gha
cache-to: type=gha

1
.gitignore vendored
View File

@@ -11,4 +11,3 @@ dist
beszel/cmd/hub/hub beszel/cmd/hub/hub
beszel/cmd/agent/agent beszel/cmd/agent/agent
node_modules node_modules
package-lock.json

View File

@@ -11,7 +11,7 @@ require (
github.com/pocketbase/dbx v1.10.1 github.com/pocketbase/dbx v1.10.1
github.com/pocketbase/pocketbase v0.22.21 github.com/pocketbase/pocketbase v0.22.21
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.8 github.com/shirou/gopsutil/v4 v4.24.9
github.com/spf13/cobra v1.8.1 github.com/spf13/cobra v1.8.1
golang.org/x/crypto v0.27.0 golang.org/x/crypto v0.27.0
) )
@@ -25,7 +25,7 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.27.39 // indirect github.com/aws/aws-sdk-go-v2/config v1.27.39 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.37 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.37 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.25 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.26 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 // 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
@@ -34,7 +34,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.63.3 // indirect github.com/aws/aws-sdk-go-v2/service/s3 v1.64.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.23.3 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.23.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.31.3 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.31.3 // indirect
@@ -43,6 +43,7 @@ require (
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
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.0 // indirect
github.com/fatih/color v1.17.0 // indirect github.com/fatih/color v1.17.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // indirect github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/ganigeorgiev/fexpr v0.4.1 // indirect github.com/ganigeorgiev/fexpr v0.4.1 // indirect
@@ -67,7 +68,6 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/spf13/cast v1.7.0 // indirect github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/tcnksm/go-gitconfig v0.1.2 // indirect github.com/tcnksm/go-gitconfig v0.1.2 // indirect
@@ -90,8 +90,8 @@ require (
golang.org/x/time v0.6.0 // indirect golang.org/x/time v0.6.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/api v0.199.0 // indirect google.golang.org/api v0.199.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f // indirect
google.golang.org/grpc v1.67.0 // indirect google.golang.org/grpc v1.67.1 // 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.61.0 // indirect modernc.org/libc v1.61.0 // indirect

View File

@@ -36,8 +36,8 @@ github.com/aws/aws-sdk-go-v2/credentials v1.17.37 h1:G2aOH01yW8X373JK419THj5QVqu
github.com/aws/aws-sdk-go-v2/credentials v1.17.37/go.mod h1:0ecCjlb7htYCptRD45lXJ6aJDQac6D2NlKGpZqyTG6A= github.com/aws/aws-sdk-go-v2/credentials v1.17.37/go.mod h1:0ecCjlb7htYCptRD45lXJ6aJDQac6D2NlKGpZqyTG6A=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 h1:C/d03NAmh8C4BZXhuRNboF/DqhBkBCeDiJDcaqIT5pA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 h1:C/d03NAmh8C4BZXhuRNboF/DqhBkBCeDiJDcaqIT5pA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14/go.mod h1:7I0Ju7p9mCIdlrfS+JCgqcYD0VXz/N4yozsox+0o078= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14/go.mod h1:7I0Ju7p9mCIdlrfS+JCgqcYD0VXz/N4yozsox+0o078=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.25 h1:HkpHeZMM39sGtMHVYG1buAg93vhj5d7F81y6G0OAbGc= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.26 h1:BTfwWNFVGLxW2bih/V2xhgCsYDQwG1cAWhWoW9Jx7wE=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.25/go.mod h1:j3Vz04ZjaWA6kygOsZRpmWe4CyGqfqq2u3unDTU0QGA= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.26/go.mod h1:LA1/FxoEFFmv7XpkB8KKqLAUz8AePdK9H0Ec7PUKazs=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 h1:kYQ3H1u0ANr9KEKlGs/jTLrBFPo8P8NaH/w7A01NeeM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 h1:kYQ3H1u0ANr9KEKlGs/jTLrBFPo8P8NaH/w7A01NeeM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18/go.mod h1:r506HmK5JDUh9+Mw4CfGJGSSoqIiLCndAuqXuhbv67Y= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18/go.mod h1:r506HmK5JDUh9+Mw4CfGJGSSoqIiLCndAuqXuhbv67Y=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 h1:Z7IdFUONvTcvS7YuhtVxN99v2cCoHRXOS4mTr0B/pUc= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 h1:Z7IdFUONvTcvS7YuhtVxN99v2cCoHRXOS4mTr0B/pUc=
@@ -54,8 +54,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 h1:Xbwbmk44
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20/go.mod h1:oAfOFzUB14ltPZj1rWwRc3d/6OgD76R8KlvU3EqM9Fg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20/go.mod h1:oAfOFzUB14ltPZj1rWwRc3d/6OgD76R8KlvU3EqM9Fg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18 h1:eb+tFOIl9ZsUe2259/BKPeniKuz4/02zZFH/i4Nf8Rg= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18 h1:eb+tFOIl9ZsUe2259/BKPeniKuz4/02zZFH/i4Nf8Rg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18/go.mod h1:GVCC2IJNJTmdlyEsSmofEy7EfJncP7DNnXDzRjJ5Keg= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18/go.mod h1:GVCC2IJNJTmdlyEsSmofEy7EfJncP7DNnXDzRjJ5Keg=
github.com/aws/aws-sdk-go-v2/service/s3 v1.63.3 h1:3zt8qqznMuAZWDTDpcwv9Xr11M/lVj2FsRR7oYBt0OA= github.com/aws/aws-sdk-go-v2/service/s3 v1.64.0 h1:I0p8knB/IDYSQ3dbanaCr4UhiYQ96bvKRhGYxvLyiD8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.63.3/go.mod h1:NLTqRLe3pUNu3nTEHI6XlHLKYmc8fbHUdMxAB6+s41Q= github.com/aws/aws-sdk-go-v2/service/s3 v1.64.0/go.mod h1:NLTqRLe3pUNu3nTEHI6XlHLKYmc8fbHUdMxAB6+s41Q=
github.com/aws/aws-sdk-go-v2/service/sso v1.23.3 h1:rs4JCczF805+FDv2tRhZ1NU0RB2H6ryAvsWPanAr72Y= github.com/aws/aws-sdk-go-v2/service/sso v1.23.3 h1:rs4JCczF805+FDv2tRhZ1NU0RB2H6ryAvsWPanAr72Y=
github.com/aws/aws-sdk-go-v2/service/sso v1.23.3/go.mod h1:XRlMvmad0ZNL+75C5FYdMvbbLkd6qiqz6foR1nA1PXY= github.com/aws/aws-sdk-go-v2/service/sso v1.23.3/go.mod h1:XRlMvmad0ZNL+75C5FYdMvbbLkd6qiqz6foR1nA1PXY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3 h1:S7EPdMVZod8BGKQQPTBK+FcX9g7bKR7c4+HxWqHP7Vg= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3 h1:S7EPdMVZod8BGKQQPTBK+FcX9g7bKR7c4+HxWqHP7Vg=
@@ -84,6 +84,8 @@ github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCO
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c= github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -227,12 +229,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.8 h1:pVQjIenQkIhqO81mwTaXjTzOMT7d3TZkf43PlVFHENI= github.com/shirou/gopsutil/v4 v4.24.9 h1:KIV+/HaHD5ka5f570RZq+2SaeFsb/pq+fp2DGNWYoOI=
github.com/shirou/gopsutil/v4 v4.24.8/go.mod h1:wE0OrJtj4dG+hYkxqDH3QiBICdKSf04/npcvLLc/oRg= github.com/shirou/gopsutil/v4 v4.24.9/go.mod h1:3fkaHNeYsUFCGZ8+9vZVWtbyM1k2eRnlL+bWO8Bxa/Q=
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/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
@@ -379,15 +377,15 @@ google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eY
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4= google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 h1:N9BgCIAUvn/M+p4NJccWPWb3BWh88+zyL0ll9HgbEeM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f h1:cUMEy+8oS78BWIH9OWazBkzbr090Od9tWBNtZHkOhf0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f/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.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
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=

View File

@@ -2,44 +2,33 @@
package agent package agent
import ( import (
"beszel/internal/entities/container"
"beszel/internal/entities/system" "beszel/internal/entities/system"
"context" "context"
"log/slog" "log/slog"
"net/http"
"os" "os"
"strings" "strings"
"sync"
"github.com/shirou/gopsutil/v4/common" "github.com/shirou/gopsutil/v4/common"
) )
type Agent struct { type Agent struct {
hostname string // Hostname of the system debug bool // true if LOG_LEVEL is set to debug
kernelVersion string // Kernel version of the system zfs bool // true if system has arcstats
cpuModel string // CPU model of the system memCalc string // Memory calculation formula
cores int // Number of cores of the system fsNames []string // List of filesystem device names being monitored
threads int // Number of threads of the system fsStats map[string]*system.FsStats // Keeps track of disk stats for each filesystem
debug bool // true if LOG_LEVEL is set to debug netInterfaces map[string]struct{} // Stores all valid network interfaces
fsNames []string // List of filesystem device names being monitored netIoStats system.NetIoStats // Keeps track of bandwidth usage
fsStats map[string]*system.FsStats // Keeps track of disk stats for each filesystem dockerManager *dockerManager // Manages Docker API requests
netInterfaces map[string]struct{} // Stores all valid network interfaces sensorsContext context.Context // Sensors context to override sys location
netIoStats system.NetIoStats // Keeps track of bandwidth usage sensorsWhitelist map[string]struct{} // List of sensors to monitor
containerStatsMap map[string]*container.Stats // Keeps track of container stats systemInfo system.Info // Host system info
containerStatsMutex sync.RWMutex // Mutex to prevent concurrent access to prevContainerStatsMap
dockerClient *http.Client // HTTP client to query docker api
apiContainerList *[]container.ApiInfo // List of containers from docker host
sensorsContext context.Context // Sensors context to override sys location
sensorsWhitelist map[string]struct{} // List of sensors to monitor
} }
func NewAgent() *Agent { func NewAgent() *Agent {
return &Agent{ return &Agent{
containerStatsMap: make(map[string]*container.Stats), sensorsContext: context.Background(),
containerStatsMutex: sync.RWMutex{}, memCalc: os.Getenv("MEM_CALC"),
netIoStats: system.NetIoStats{},
dockerClient: newDockerClient(),
sensorsContext: context.Background(),
} }
} }
@@ -73,22 +62,32 @@ func (a *Agent) Run(pubKey []byte, addr string) {
} }
} }
// initialize system info / docker manager
a.initializeSystemInfo() a.initializeSystemInfo()
a.initializeDiskInfo() a.initializeDiskInfo()
a.initializeNetIoStats() a.initializeNetIoStats()
a.dockerManager = newDockerManager()
a.startServer(pubKey, addr) // if debugging, print stats
if a.debug {
slog.Debug("Stats", "data", a.GatherStats())
}
if pubKey != nil {
a.startServer(pubKey, addr)
}
} }
func (a *Agent) gatherStats() system.CombinedData { func (a *Agent) GatherStats() system.CombinedData {
systemInfo, SystemStats := a.getSystemStats()
systemData := system.CombinedData{ systemData := system.CombinedData{
Stats: SystemStats, Stats: a.getSystemStats(),
Info: systemInfo, Info: a.systemInfo,
} }
// add docker stats // add docker stats
if containerStats, err := a.getDockerStats(); err == nil { if containerStats, err := a.dockerManager.getDockerStats(); err == nil {
systemData.Containers = containerStats systemData.Containers = containerStats
} else {
slog.Debug("Error getting docker stats", "err", err)
} }
// add extra filesystems // add extra filesystems
systemData.Stats.ExtraFs = make(map[string]*system.FsStats) systemData.Stats.ExtraFs = make(map[string]*system.FsStats)

View File

@@ -13,90 +13,110 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/blang/semver"
) )
type dockerManager struct {
client *http.Client // Client to query Docker API
wg sync.WaitGroup // WaitGroup to wait for all goroutines to finish
sem chan struct{} // Semaphore to limit concurrent container requests
containerStatsMutex sync.RWMutex // Mutex to prevent concurrent access to containerStatsMap
apiContainerList *[]container.ApiInfo // List of containers from Docker API
containerStatsMap map[string]*container.Stats // Keeps track of container stats
validIds map[string]struct{} // Map of valid container ids, used to prune invalid containers from containerStatsMap
}
// Add goroutine to the queue
func (d *dockerManager) queue() {
d.sem <- struct{}{}
d.wg.Add(1)
}
// Remove goroutine from the queue
func (d *dockerManager) dequeue() {
<-d.sem
d.wg.Done()
}
// Returns stats for all running containers // Returns stats for all running containers
func (a *Agent) getDockerStats() ([]*container.Stats, error) { func (dm *dockerManager) getDockerStats() ([]*container.Stats, error) {
resp, err := a.dockerClient.Get("http://localhost/containers/json") resp, err := dm.client.Get("http://localhost/containers/json")
if err != nil { if err != nil {
a.closeIdleConnections(err)
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&a.apiContainerList); err != nil { if err := json.NewDecoder(resp.Body).Decode(&dm.apiContainerList); err != nil {
slog.Error("Error decoding containers", "err", err)
return nil, err return nil, err
} }
containersLength := len(*a.apiContainerList) containersLength := len(*dm.apiContainerList)
containerStats := make([]*container.Stats, containersLength)
// 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{}, containersLength) if dm.validIds == nil {
dm.validIds = make(map[string]struct{}, containersLength)
} else {
clear(dm.validIds)
}
var wg sync.WaitGroup for _, ctr := range *dm.apiContainerList {
for i, ctr := range *a.apiContainerList {
ctr.IdShort = ctr.Id[:12] ctr.IdShort = ctr.Id[:12]
validIds[ctr.IdShort] = struct{}{} dm.validIds[ctr.IdShort] = struct{}{}
// check if container is less than 1 minute old (possible restart) // check if container is less than 1 minute old (possible restart)
// note: can't use Created field because it's not updated on restart // note: can't use Created field because it's not updated on restart
if strings.Contains(ctr.Status, "second") { if strings.Contains(ctr.Status, "second") {
// if so, remove old container data // if so, remove old container data
a.deleteContainerStatsSync(ctr.IdShort) dm.deleteContainerStatsSync(ctr.IdShort)
} }
wg.Add(1) dm.queue()
go func() { go func() {
defer wg.Done() defer dm.dequeue()
stats, err := a.getContainerStats(ctr) err := dm.updateContainerStats(ctr)
if err != nil { if err != nil {
// close idle connections if error is a network timeout dm.deleteContainerStatsSync(ctr.IdShort)
isTimeout := a.closeIdleConnections(err)
// delete container from map if not a timeout
if !isTimeout {
a.deleteContainerStatsSync(ctr.IdShort)
}
// retry once // retry once
stats, err = a.getContainerStats(ctr) err = dm.updateContainerStats(ctr)
if err != nil { if err != nil {
slog.Error("Error getting container stats", "err", err) slog.Error("Error getting container stats", "err", err)
} }
} }
containerStats[i] = stats
}() }()
} }
wg.Wait() dm.wg.Wait()
// remove old / invalid container stats // populate final stats and remove old / invalid container stats
for id := range a.containerStatsMap { stats := make([]*container.Stats, 0, containersLength)
if _, exists := validIds[id]; !exists { for id, v := range dm.containerStatsMap {
delete(a.containerStatsMap, id) if _, exists := dm.validIds[id]; !exists {
delete(dm.containerStatsMap, id)
} else {
stats = append(stats, v)
} }
} }
return containerStats, nil return stats, nil
} }
// Returns stats for individual container // Updates stats for individual container
func (a *Agent) getContainerStats(ctr container.ApiInfo) (*container.Stats, error) { func (dm *dockerManager) updateContainerStats(ctr container.ApiInfo) error {
name := ctr.Names[0][1:] name := ctr.Names[0][1:]
resp, err := a.dockerClient.Get("http://localhost/containers/" + ctr.IdShort + "/stats?stream=0&one-shot=1") resp, err := dm.client.Get("http://localhost/containers/" + ctr.IdShort + "/stats?stream=0&one-shot=1")
if err != nil { if err != nil {
return &container.Stats{Name: name}, err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
a.containerStatsMutex.Lock() dm.containerStatsMutex.Lock()
defer a.containerStatsMutex.Unlock() defer dm.containerStatsMutex.Unlock()
// add empty values if they doesn't exist in map // add empty values if they doesn't exist in map
stats, initialized := a.containerStatsMap[ctr.IdShort] stats, initialized := dm.containerStatsMap[ctr.IdShort]
if !initialized { if !initialized {
stats = &container.Stats{Name: name} stats = &container.Stats{Name: name}
a.containerStatsMap[ctr.IdShort] = stats dm.containerStatsMap[ctr.IdShort] = stats
} }
// reset current stats // reset current stats
@@ -108,12 +128,12 @@ func (a *Agent) getContainerStats(ctr container.ApiInfo) (*container.Stats, erro
// docker host container stats response // docker host container stats response
var res container.ApiStats var res container.ApiStats
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return stats, err return err
} }
// 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 res.MemoryStats.Usage == 0 { if res.MemoryStats.Usage == 0 {
return stats, fmt.Errorf("%s - no memory stats - see https://github.com/henrygd/beszel/issues/144", name) return fmt.Errorf("%s - no memory stats - see https://github.com/henrygd/beszel/issues/144", name)
} }
// memory (https://docs.docker.com/reference/cli/docker/container/stats/) // memory (https://docs.docker.com/reference/cli/docker/container/stats/)
@@ -128,7 +148,7 @@ func (a *Agent) getContainerStats(ctr container.ApiInfo) (*container.Stats, erro
systemDelta := res.CPUStats.SystemUsage - stats.PrevCpu[1] systemDelta := res.CPUStats.SystemUsage - stats.PrevCpu[1]
cpuPct := float64(cpuDelta) / float64(systemDelta) * 100 cpuPct := float64(cpuDelta) / float64(systemDelta) * 100
if cpuPct > 100 { if cpuPct > 100 {
return stats, fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct) return fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct)
} }
stats.PrevCpu = [2]uint64{res.CPUStats.CPUUsage.TotalUsage, res.CPUStats.SystemUsage} stats.PrevCpu = [2]uint64{res.CPUStats.CPUUsage.TotalUsage, res.CPUStats.SystemUsage}
@@ -154,11 +174,18 @@ func (a *Agent) getContainerStats(ctr container.ApiInfo) (*container.Stats, erro
stats.NetworkSent = bytesToMegabytes(sent_delta) stats.NetworkSent = bytesToMegabytes(sent_delta)
stats.NetworkRecv = bytesToMegabytes(recv_delta) stats.NetworkRecv = bytesToMegabytes(recv_delta)
return stats, nil return nil
} }
// Creates a new http client for docker api // Delete container stats from map using mutex
func newDockerClient() *http.Client { func (dm *dockerManager) deleteContainerStatsSync(id string) {
dm.containerStatsMutex.Lock()
defer dm.containerStatsMutex.Unlock()
delete(dm.containerStatsMap, id)
}
// Creates a new http client for Docker API
func newDockerManager() *dockerManager {
dockerHost := "unix:///var/run/docker.sock" dockerHost := "unix:///var/run/docker.sock"
if dockerHostEnv, exists := os.LookupEnv("DOCKER_HOST"); exists { if dockerHostEnv, exists := os.LookupEnv("DOCKER_HOST"); exists {
slog.Info("DOCKER_HOST", "host", dockerHostEnv) slog.Info("DOCKER_HOST", "host", dockerHostEnv)
@@ -172,12 +199,8 @@ func newDockerClient() *http.Client {
} }
transport := &http.Transport{ transport := &http.Transport{
ForceAttemptHTTP2: false, DisableCompression: true,
IdleConnTimeout: 90 * time.Second, MaxConnsPerHost: 0,
DisableCompression: true,
MaxConnsPerHost: 10,
MaxIdleConnsPerHost: 10,
DisableKeepAlives: false,
} }
switch parsedURL.Scheme { switch parsedURL.Scheme {
@@ -194,18 +217,37 @@ func newDockerClient() *http.Client {
os.Exit(1) os.Exit(1)
} }
return &http.Client{ dockerClient := &dockerManager{
Timeout: time.Second, client: &http.Client{
Transport: transport, Timeout: time.Millisecond * 1100,
Transport: transport,
},
containerStatsMap: make(map[string]*container.Stats),
} }
}
// Closes idle connections on timeouts to prevent reuse of stale connections // Make sure sem is initialized
func (a *Agent) closeIdleConnections(err error) (isTimeout bool) { concurrency := 200
if netErr, ok := err.(net.Error); ok && netErr.Timeout() { defer func() { dockerClient.sem = make(chan struct{}, concurrency) }()
slog.Warn("Closing idle connections", "err", err)
a.dockerClient.Transport.(*http.Transport).CloseIdleConnections() // Check docker version
return true // (versions before 25.0.0 have a bug with one-shot which requires all requests to be made in one batch)
var versionInfo struct {
Version string `json:"Version"`
} }
return false resp, err := dockerClient.client.Get("http://localhost/version")
if err != nil {
return dockerClient
}
if err := json.NewDecoder(resp.Body).Decode(&versionInfo); err != nil {
return dockerClient
}
// if version > 25, one-shot works correctly and we can limit concurrent connections / goroutines to 5
if dockerVersion, err := semver.Parse(versionInfo.Version); err == nil && dockerVersion.Major > 24 {
concurrency = 5
}
slog.Debug("Docker", "version", versionInfo.Version, "concurrency", concurrency)
return dockerClient
} }

View File

@@ -24,7 +24,7 @@ func (a *Agent) startServer(pubKey []byte, addr string) {
} }
func (a *Agent) handleSession(s sshServer.Session) { func (a *Agent) handleSession(s sshServer.Session) {
stats := a.gatherStats() stats := a.GatherStats()
slog.Debug("Sending stats", "data", stats) slog.Debug("Sending stats", "data", stats)
if err := json.NewEncoder(s).Encode(stats); err != nil { if err := json.NewEncoder(s).Encode(stats); err != nil {
slog.Error("Error encoding stats", "err", err) slog.Error("Error encoding stats", "err", err)

View File

@@ -3,9 +3,12 @@ package agent
import ( import (
"beszel" "beszel"
"beszel/internal/entities/system" "beszel/internal/entities/system"
"bufio"
"fmt"
"log/slog" "log/slog"
"os" "os"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/shirou/gopsutil/v4/cpu" "github.com/shirou/gopsutil/v4/cpu"
@@ -18,26 +21,35 @@ import (
// Sets initial / non-changing values about the host system // Sets initial / non-changing values about the host system
func (a *Agent) initializeSystemInfo() { func (a *Agent) initializeSystemInfo() {
a.kernelVersion, _ = host.KernelVersion() a.systemInfo.AgentVersion = beszel.Version
a.hostname, _ = os.Hostname() a.systemInfo.Hostname, _ = os.Hostname()
a.systemInfo.KernelVersion, _ = host.KernelVersion()
// add cpu stats // cpu model
if info, err := cpu.Info(); err == nil && len(info) > 0 { if info, err := cpu.Info(); err == nil && len(info) > 0 {
a.cpuModel = info[0].ModelName a.systemInfo.CpuModel = info[0].ModelName
} }
a.cores, _ = cpu.Counts(false) // cores / threads
a.systemInfo.Cores, _ = cpu.Counts(false)
if threads, err := cpu.Counts(true); err == nil { if threads, err := cpu.Counts(true); err == nil {
if threads > 0 && threads < a.cores { if threads > 0 && threads < a.systemInfo.Cores {
// in lxc logical cores reflects container limits, so use that as cores if lower // in lxc logical cores reflects container limits, so use that as cores if lower
a.cores = threads a.systemInfo.Cores = threads
} else { } else {
a.threads = threads a.systemInfo.Threads = threads
} }
} }
// zfs
if _, err := getARCSize(); err == nil {
a.zfs = true
} else {
slog.Debug("Not monitoring ZFS ARC", "err", err)
}
} }
// Returns current info, stats about the host system // Returns current info, stats about the host system
func (a *Agent) getSystemStats() (system.Info, system.Stats) { func (a *Agent) getSystemStats() system.Stats {
systemStats := system.Stats{} systemStats := system.Stats{}
// cpu percent // cpu percent
@@ -50,12 +62,30 @@ func (a *Agent) getSystemStats() (system.Info, system.Stats) {
// memory // memory
if v, err := mem.VirtualMemory(); err == nil { if v, err := mem.VirtualMemory(); err == nil {
systemStats.Mem = bytesToGigabytes(v.Total) // swap
systemStats.MemUsed = bytesToGigabytes(v.Used)
systemStats.MemBuffCache = bytesToGigabytes(v.Total - v.Free - v.Used)
systemStats.MemPct = twoDecimals(v.UsedPercent)
systemStats.Swap = bytesToGigabytes(v.SwapTotal) systemStats.Swap = bytesToGigabytes(v.SwapTotal)
systemStats.SwapUsed = bytesToGigabytes(v.SwapTotal - v.SwapFree) systemStats.SwapUsed = bytesToGigabytes(v.SwapTotal - v.SwapFree - v.SwapCached)
// cache + buffers value for default mem calculation
cacheBuff := v.Total - v.Free - v.Used
// htop memory calculation overrides
if a.memCalc == "htop" {
// note: gopsutil automatically adds SReclaimable to v.Cached
cacheBuff = v.Cached + v.Buffers - v.Shared
v.Used = v.Total - (v.Free + cacheBuff)
v.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0
}
// subtract ZFS ARC size from used memory and add as its own category
if a.zfs {
if arcSize, _ := getARCSize(); arcSize > 0 && arcSize < v.Used {
v.Used = v.Used - arcSize
v.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0
systemStats.MemZfsArc = bytesToGigabytes(arcSize)
}
}
systemStats.Mem = bytesToGigabytes(v.Total)
systemStats.MemBuffCache = bytesToGigabytes(cacheBuff)
systemStats.MemUsed = bytesToGigabytes(v.Used)
systemStats.MemPct = twoDecimals(v.UsedPercent)
} }
// disk usage // disk usage
@@ -152,7 +182,7 @@ func (a *Agent) getSystemStats() (system.Info, system.Stats) {
systemStats.Temperatures = make(map[string]float64, len(temps)) systemStats.Temperatures = make(map[string]float64, len(temps))
for i, sensor := range temps { for i, sensor := range temps {
// skip if temperature is 0 // skip if temperature is 0
if sensor.Temperature == 0 { if sensor.Temperature <= 0 || sensor.Temperature >= 200 {
continue continue
} }
if _, ok := systemStats.Temperatures[sensor.SensorKey]; ok { if _, ok := systemStats.Temperatures[sensor.SensorKey]; ok {
@@ -173,19 +203,37 @@ func (a *Agent) getSystemStats() (system.Info, system.Stats) {
} }
} }
systemInfo := system.Info{ // update base system info
Cpu: systemStats.Cpu, a.systemInfo.Cpu = systemStats.Cpu
MemPct: systemStats.MemPct, a.systemInfo.MemPct = systemStats.MemPct
DiskPct: systemStats.DiskPct, a.systemInfo.DiskPct = systemStats.DiskPct
AgentVersion: beszel.Version, a.systemInfo.Uptime, _ = host.Uptime()
Hostname: a.hostname,
KernelVersion: a.kernelVersion, return systemStats
CpuModel: a.cpuModel, }
Cores: a.cores,
Threads: a.threads, // Returns the size of the ZFS ARC memory cache in bytes
func getARCSize() (uint64, error) {
file, err := os.Open("/proc/spl/kstat/zfs/arcstats")
if err != nil {
return 0, err
}
defer file.Close()
// Scan the lines
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "size") {
// Example line: size 4 15032385536
fields := strings.Fields(line)
if len(fields) < 3 {
return 0, err
}
// Return the size as uint64
return strconv.ParseUint(fields[2], 10, 64)
}
} }
systemInfo.Uptime, _ = host.Uptime() return 0, fmt.Errorf("failed to parse size field")
return systemInfo, systemStats
} }

View File

@@ -2,13 +2,6 @@ package agent
import "math" import "math"
// delete container stats from map using mutex
func (a *Agent) deleteContainerStatsSync(id string) {
a.containerStatsMutex.Lock()
defer a.containerStatsMutex.Unlock()
delete(a.containerStatsMap, id)
}
func bytesToMegabytes(b float64) float64 { func bytesToMegabytes(b float64) float64 {
return twoDecimals(b / 1048576) return twoDecimals(b / 1048576)
} }

View File

@@ -11,6 +11,7 @@ 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"`
MemZfsArc float64 `json:"mz,omitempty"` // ZFS ARC memory
Swap float64 `json:"s,omitempty"` Swap float64 `json:"s,omitempty"`
SwapUsed float64 `json:"su,omitempty"` SwapUsed float64 `json:"su,omitempty"`
DiskTotal float64 `json:"d"` DiskTotal float64 `json:"d"`
@@ -44,17 +45,16 @@ type NetIoStats struct {
} }
type Info struct { type Info struct {
Hostname string `json:"h"` Hostname string `json:"h"`
KernelVersion string `json:"k,omitempty"` KernelVersion string `json:"k,omitempty"`
Cores int `json:"c"` Cores int `json:"c"`
Threads int `json:"t,omitempty"` Threads int `json:"t,omitempty"`
CpuModel string `json:"m"` CpuModel string `json:"m"`
// Os string `json:"o"` Uptime uint64 `json:"u"`
Uptime uint64 `json:"u"` Cpu float64 `json:"cpu"`
Cpu float64 `json:"cpu"` MemPct float64 `json:"mp"`
MemPct float64 `json:"mp"` DiskPct float64 `json:"dp"`
DiskPct float64 `json:"dp"` AgentVersion string `json:"v"`
AgentVersion string `json:"v"`
} }
// Final data structure to return to the hub // Final data structure to return to the hub

View File

@@ -3,6 +3,7 @@ package hub
import ( import (
"beszel" "beszel"
"beszel/internal/agent"
"beszel/internal/alerts" "beszel/internal/alerts"
"beszel/internal/entities/system" "beszel/internal/entities/system"
"beszel/internal/records" "beszel/internal/records"
@@ -42,6 +43,7 @@ type Hub struct {
am *alerts.AlertManager am *alerts.AlertManager
um *users.UserManager um *users.UserManager
rm *records.RecordManager rm *records.RecordManager
hubAgent *agent.Agent
} }
func NewHub(app *pocketbase.PocketBase) *Hub { func NewHub(app *pocketbase.PocketBase) *Hub {
@@ -56,10 +58,6 @@ func NewHub(app *pocketbase.PocketBase) *Hub {
} }
func (h *Hub) Run() { func (h *Hub) Run() {
// rm := records.NewRecordManager(h.app)
// am := alerts.NewAlertManager(h.app)
// um := users.NewUserManager(h.app)
// loosely check if it was executed using "go run" // loosely check if it was executed using "go run"
isGoRun := strings.HasPrefix(os.Args[0], os.TempDir()) isGoRun := strings.HasPrefix(os.Args[0], os.TempDir())
@@ -73,25 +71,22 @@ func (h *Hub) Run() {
// initial setup // initial setup
h.app.OnBeforeServe().Add(func(e *core.ServeEvent) error { h.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// create ssh client config // create ssh client config
err := h.createSSHClientConfig() if err := h.createSSHClientConfig(); err != nil {
if err != nil {
log.Fatal(err) log.Fatal(err)
} }
// set auth settings // set auth settings
usersCollection, err := h.app.Dao().FindCollectionByNameOrId("users") if usersCollection, err := h.app.Dao().FindCollectionByNameOrId("users"); err == nil {
if err != nil { usersAuthOptions := usersCollection.AuthOptions()
return err usersAuthOptions.AllowUsernameAuth = false
} if os.Getenv("DISABLE_PASSWORD_AUTH") == "true" {
usersAuthOptions := usersCollection.AuthOptions() usersAuthOptions.AllowEmailAuth = false
usersAuthOptions.AllowUsernameAuth = false } else {
if os.Getenv("DISABLE_PASSWORD_AUTH") == "true" { usersAuthOptions.AllowEmailAuth = true
usersAuthOptions.AllowEmailAuth = false }
} else { usersCollection.SetOptions(usersAuthOptions)
usersAuthOptions.AllowEmailAuth = true if err := h.app.Dao().SaveCollection(usersCollection); err != nil {
} return err
usersCollection.SetOptions(usersAuthOptions) }
if err := h.app.Dao().SaveCollection(usersCollection); err != nil {
return err
} }
return nil return nil
}) })
@@ -159,6 +154,16 @@ func (h *Hub) Run() {
// system creation defaults // system creation defaults
h.app.OnModelBeforeCreate("systems").Add(func(e *core.ModelEvent) error { h.app.OnModelBeforeCreate("systems").Add(func(e *core.ModelEvent) error {
record := e.Model.(*models.Record) record := e.Model.(*models.Record)
if record.GetString("host") == "hubsys" {
// todo: check for hubsys existance and return error if exists (or make sure user is admin)
if record.GetString("name") == "x" {
hostname, _ := os.Hostname()
if hostname == "" {
hostname = "localhost"
}
record.Set("name", hostname)
}
}
record.Set("info", system.Info{}) record.Set("info", system.Info{})
record.Set("status", "pending") record.Set("status", "pending")
return nil return nil
@@ -246,6 +251,26 @@ func (h *Hub) updateSystems() {
} }
func (h *Hub) updateSystem(record *models.Record) { func (h *Hub) updateSystem(record *models.Record) {
switch record.GetString("host") {
case "hubsys":
h.updateHubSystem(record)
default:
h.updateRemoteSystem(record)
}
}
// Update hub system stats with built-in agent
func (h *Hub) updateHubSystem(record *models.Record) {
if h.hubAgent == nil {
h.hubAgent = agent.NewAgent()
h.hubAgent.Run(nil, "")
}
systemData := h.hubAgent.GatherStats()
h.saveSystemStats(record, &systemData)
}
// Connect to remote system and update system stats
func (h *Hub) updateRemoteSystem(record *models.Record) {
var client *ssh.Client var client *ssh.Client
var err error var err error
@@ -273,7 +298,7 @@ func (h *Hub) updateSystem(record *models.Record) {
// if previous connection was closed, try again // if previous connection was closed, try again
h.app.Logger().Error("Existing SSH connection closed. Retrying...", "host", record.GetString("host"), "port", record.GetString("port")) h.app.Logger().Error("Existing SSH connection closed. Retrying...", "host", record.GetString("host"), "port", record.GetString("port"))
h.deleteSystemConnection(record) h.deleteSystemConnection(record)
h.updateSystem(record) h.updateRemoteSystem(record)
return return
} }
h.app.Logger().Error("Failed to get system stats: ", "err", err.Error()) h.app.Logger().Error("Failed to get system stats: ", "err", err.Error())
@@ -281,6 +306,11 @@ func (h *Hub) updateSystem(record *models.Record) {
return return
} }
// update system record // update system record
h.saveSystemStats(record, &systemData)
}
// Update system record with provided system.CombinedData
func (h *Hub) saveSystemStats(record *models.Record, systemData *system.CombinedData) {
record.Set("status", "up") record.Set("status", "up")
record.Set("info", systemData.Info) record.Set("info", systemData.Info)
if err := h.app.Dao().SaveRecord(record); err != nil { if err := h.app.Dao().SaveRecord(record); err != nil {
@@ -320,14 +350,20 @@ func (h *Hub) updateSystemStatus(record *models.Record, status string) {
} }
} }
// Deletes the SSH connection (remote) or built-in agent reference
func (h *Hub) deleteSystemConnection(record *models.Record) { func (h *Hub) deleteSystemConnection(record *models.Record) {
if _, ok := h.systemConnections[record.Id]; ok { switch record.GetString("host") {
if h.systemConnections[record.Id] != nil { case "hubsys":
h.systemConnections[record.Id].Close() h.hubAgent = nil
default:
if _, ok := h.systemConnections[record.Id]; ok {
if h.systemConnections[record.Id] != nil {
h.systemConnections[record.Id].Close()
}
h.connectionLock.Lock()
defer h.connectionLock.Unlock()
delete(h.systemConnections, record.Id)
} }
h.connectionLock.Lock()
defer h.connectionLock.Unlock()
delete(h.systemConnections, record.Id)
} }
} }

View File

@@ -161,6 +161,7 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
sum.MemUsed += stats.MemUsed sum.MemUsed += stats.MemUsed
sum.MemPct += stats.MemPct sum.MemPct += stats.MemPct
sum.MemBuffCache += stats.MemBuffCache sum.MemBuffCache += stats.MemBuffCache
sum.MemZfsArc += stats.MemZfsArc
sum.Swap += stats.Swap sum.Swap += stats.Swap
sum.SwapUsed += stats.SwapUsed sum.SwapUsed += stats.SwapUsed
sum.DiskTotal += stats.DiskTotal sum.DiskTotal += stats.DiskTotal
@@ -200,6 +201,7 @@ func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Sta
MemUsed: twoDecimals(sum.MemUsed / count), MemUsed: twoDecimals(sum.MemUsed / count),
MemPct: twoDecimals(sum.MemPct / count), MemPct: twoDecimals(sum.MemPct / count),
MemBuffCache: twoDecimals(sum.MemBuffCache / count), MemBuffCache: twoDecimals(sum.MemBuffCache / count),
MemZfsArc: twoDecimals(sum.MemZfsArc / count),
Swap: twoDecimals(sum.Swap / count), Swap: twoDecimals(sum.Swap / count),
SwapUsed: twoDecimals(sum.SwapUsed / count), SwapUsed: twoDecimals(sum.SwapUsed / count),
DiskTotal: twoDecimals(sum.DiskTotal / count), DiskTotal: twoDecimals(sum.DiskTotal / count),

Binary file not shown.

4916
beszel/site/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{ {
"name": "site", "name": "beszel",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
@@ -23,7 +23,7 @@
"@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.5", "@tanstack/react-table": "^8.20.5",
"@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react": "^4.3.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
@@ -34,20 +34,19 @@
"pocketbase": "^0.21.5", "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.5",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"use-is-in-viewport": "^1.0.9",
"valibot": "^0.36.0" "valibot": "^0.36.0"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.1.8", "@types/bun": "^1.1.10",
"@types/react": "^18.3.5", "@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"postcss": "^8.4.44", "postcss": "^8.4.47",
"tailwindcss": "^3.4.10", "tailwindcss": "^3.4.13",
"typescript": "^5.5.4", "typescript": "^5.6.2",
"vite": "^5.4.2" "vite": "^5.4.8"
} }
} }

View File

@@ -5,12 +5,11 @@ import {
useYAxisWidth, useYAxisWidth,
chartTimeData, chartTimeData,
cn, cn,
formatShortDate,
toFixedFloat, toFixedFloat,
twoDecimalString, twoDecimalString,
formatShortDate,
} from '@/lib/utils' } from '@/lib/utils'
import { useMemo } from 'react' import { useMemo } from 'react'
// import Spinner from '../spinner'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores' import { $chartTime } from '@/lib/stores'
import { SystemStatsRecord } from '@/types' import { SystemStatsRecord } from '@/types'
@@ -79,7 +78,7 @@ export default function MemChart({
content={ content={
<ChartTooltipContent <ChartTooltipContent
// @ts-ignore // @ts-ignore
itemSorter={(a, b) => a.name.localeCompare(b.name)} itemSorter={(a, b) => a.order - b.order}
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)} labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={(item) => twoDecimalString(item.value) + ' GB'} contentFormatter={(item) => twoDecimalString(item.value) + ' GB'}
indicator="line" indicator="line"
@@ -87,8 +86,9 @@ export default function MemChart({
} }
/> />
<Area <Area
dataKey="stats.mu"
name="Used" name="Used"
order={3}
dataKey="stats.mu"
type="monotoneX" type="monotoneX"
fill="hsl(var(--chart-2))" fill="hsl(var(--chart-2))"
fillOpacity={0.4} fillOpacity={0.4}
@@ -96,9 +96,23 @@ export default function MemChart({
stackId="1" stackId="1"
isAnimationActive={false} isAnimationActive={false}
/> />
{systemData.at(-1)?.stats.mz && (
<Area
name="ZFS ARC"
order={2}
dataKey="stats.mz"
type="monotoneX"
fill="hsla(175 60% 45% / 0.8)"
fillOpacity={0.5}
stroke="hsla(175 60% 45% / 0.8)"
stackId="1"
isAnimationActive={false}
/>
)}
<Area <Area
dataKey="stats.mb"
name="Cache / Buffers" name="Cache / Buffers"
order={1}
dataKey="stats.mb"
type="monotoneX" type="monotoneX"
fill="hsla(160 60% 45% / 0.5)" fill="hsla(160 60% 45% / 0.5)"
fillOpacity={0.4} fillOpacity={0.4}

View File

@@ -94,13 +94,15 @@ export function UserAuthForm({
setErrors({ passwordConfirm: msg }) setErrors({ passwordConfirm: msg })
return return
} }
// create admin user
await pb.admins.create({ await pb.admins.create({
email, email,
password, password,
passwordConfirm: password, passwordConfirm: password,
}) })
await pb.admins.authWithPassword(email, password) await pb.admins.authWithPassword(email, password)
await pb.collection('users').create({ // create regular user
const user = await pb.collection('users').create({
username, username,
email, email,
password, password,
@@ -108,6 +110,13 @@ export function UserAuthForm({
role: 'admin', role: 'admin',
verified: true, verified: true,
}) })
// create hubsys
await pb.collection('systems').create({
name: 'x',
port: 'x',
host: 'hubsys',
users: user.id,
})
await pb.collection('users').authWithPassword(email, password) await pb.collection('users').authWithPassword(email, password)
} else { } else {
await pb.collection('users').authWithPassword(email, password) await pb.collection('users').authWithPassword(email, password)

View File

@@ -6,19 +6,14 @@ import { useStore } from '@nanostores/react'
import Spinner from '../spinner' import Spinner from '../spinner'
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from 'lucide-react' import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from 'lucide-react'
import ChartTimeSelect from '../charts/chart-time-select' import ChartTimeSelect from '../charts/chart-time-select'
import { import { chartTimeData, cn, getPbTimestamp, useLocalStorage } from '@/lib/utils'
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 { Button, buttonVariants } from '../ui/button'
import { Input } from '../ui/input' import { Input } from '../ui/input'
import { Rows, TuxIcon } from '../ui/icons' import { Rows, TuxIcon } from '../ui/icons'
import { useIntersectionObserver } from '@/lib/use-intersection-observer'
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'))
@@ -196,12 +191,13 @@ export default function SystemDetail({ name }: { name: string }) {
} }
let uptime: number | string = system.info.u let uptime: number | string = system.info.u
if (system.info.u < 172800) { if (system.info.u < 172800) {
uptime = `${Math.trunc(uptime / 3600)} hours` const hours = Math.trunc(uptime / 3600)
uptime = `${hours} hour${hours > 1 ? 's' : ''}`
} else { } else {
uptime = `${Math.trunc(system.info?.u / 86400)} days` uptime = `${Math.trunc(system.info?.u / 86400)} days`
} }
return [ return [
{ value: system.host, Icon: GlobeIcon }, { value: system.host, Icon: GlobeIcon, hide: system.host === 'hubsys' },
{ {
value: system.info.h, value: system.info.h,
Icon: MonitorIcon, Icon: MonitorIcon,
@@ -504,13 +500,12 @@ function ChartCard({
grid?: boolean grid?: boolean
isContainerChart?: boolean isContainerChart?: boolean
}) { }) {
const target = useRef<HTMLDivElement>(null) const { isIntersecting, ref } = useIntersectionObserver()
const [isInViewport, wrappedTargetRef] = useClampedIsInViewport({ target: target })
return ( return (
<Card <Card
className={cn('pb-2 sm:pb-4 odd:last-of-type:col-span-full', { 'col-span-full': !grid })} className={cn('pb-2 sm:pb-4 odd:last-of-type:col-span-full', { 'col-span-full': !grid })}
ref={wrappedTargetRef} ref={ref}
> >
<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>
@@ -519,7 +514,7 @@ function ChartCard({
</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 />}
{isInViewport && <Suspense>{children}</Suspense>} {isIntersecting && <Suspense>{children}</Suspense>}
</CardContent> </CardContent>
</Card> </Card>
) )

View File

@@ -0,0 +1,169 @@
import { useEffect, useRef, useState } from 'react'
// adapted from usehooks-ts/use-intersection-observer
/** The hook internal state. */
type State = {
/** A boolean indicating if the element is intersecting. */
isIntersecting: boolean
/** The intersection observer entry. */
entry?: IntersectionObserverEntry
}
/** Represents the options for configuring the Intersection Observer. */
type UseIntersectionObserverOptions = {
/**
* The element that is used as the viewport for checking visibility of the target.
* @default null
*/
root?: Element | Document | null
/**
* A margin around the root.
* @default '0%'
*/
rootMargin?: string
/**
* A threshold indicating the percentage of the target's visibility needed to trigger the callback.
* @default 0
*/
threshold?: number | number[]
/**
* If true, freezes the intersection state once the element becomes visible.
* @default true
*/
freeze?: boolean
/**
* A callback function to be invoked when the intersection state changes.
* @param {boolean} isIntersecting - A boolean indicating if the element is intersecting.
* @param {IntersectionObserverEntry} entry - The intersection observer Entry.
* @default undefined
*/
onChange?: (isIntersecting: boolean, entry: IntersectionObserverEntry) => void
/**
* The initial state of the intersection.
* @default false
*/
initialIsIntersecting?: boolean
}
/**
* The return type of the useIntersectionObserver hook.
*
* Supports both tuple and object destructing.
* @param {(node: Element | null) => void} ref - The ref callback function.
* @param {boolean} isIntersecting - A boolean indicating if the element is intersecting.
* @param {IntersectionObserverEntry | undefined} entry - The intersection observer Entry.
*/
type IntersectionReturn = {
ref: (node?: Element | null) => void
isIntersecting: boolean
entry?: IntersectionObserverEntry
}
/**
* Custom hook that tracks the intersection of a DOM element with its containing element or the viewport using the [`Intersection Observer API`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
* @param {UseIntersectionObserverOptions} options - The options for the Intersection Observer.
* @returns {IntersectionReturn} The ref callback, a boolean indicating if the element is intersecting, and the intersection observer entry.
* @example
* ```tsx
* const { ref, isIntersecting, entry } = useIntersectionObserver({ threshold: 0.5 });
* ```
*/
export function useIntersectionObserver({
threshold = 0,
root = null,
rootMargin = '0%',
freeze = true,
initialIsIntersecting = false,
onChange,
}: UseIntersectionObserverOptions = {}): IntersectionReturn {
const [ref, setRef] = useState<Element | null>(null)
const [state, setState] = useState<State>(() => ({
isIntersecting: initialIsIntersecting,
entry: undefined,
}))
const callbackRef = useRef<UseIntersectionObserverOptions['onChange']>()
callbackRef.current = onChange
const frozen = state.entry?.isIntersecting && freeze
useEffect(() => {
// Ensure we have a ref to observe
if (!ref) return
// Ensure the browser supports the Intersection Observer API
if (!('IntersectionObserver' in window)) return
// Skip if frozen
if (frozen) return
let unobserve: (() => void) | undefined
const observer = new IntersectionObserver(
(entries: IntersectionObserverEntry[]): void => {
const thresholds = Array.isArray(observer.thresholds)
? observer.thresholds
: [observer.thresholds]
entries.forEach((entry) => {
const isIntersecting =
entry.isIntersecting &&
thresholds.some((threshold) => entry.intersectionRatio >= threshold)
setState({ isIntersecting, entry })
if (callbackRef.current) {
callbackRef.current(isIntersecting, entry)
}
if (isIntersecting && freeze && unobserve) {
unobserve()
unobserve = undefined
}
})
},
{ threshold, root, rootMargin }
)
observer.observe(ref)
return () => {
observer.disconnect()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
ref,
// eslint-disable-next-line react-hooks/exhaustive-deps
JSON.stringify(threshold),
root,
rootMargin,
frozen,
freeze,
])
// ensures that if the observed element changes, the intersection observer is reinitialized
const prevRef = useRef<Element | null>(null)
useEffect(() => {
if (
!ref &&
state.entry?.target &&
!freeze &&
!frozen &&
prevRef.current !== state.entry.target
) {
prevRef.current = state.entry.target
setState({ isIntersecting: initialIsIntersecting, entry: undefined })
}
}, [ref, state.entry, freeze, frozen, initialIsIntersecting])
return {
ref: setRef,
isIntersecting: !!state.isIntersecting,
entry: state.entry,
} as IntersectionReturn
}

View File

@@ -7,7 +7,6 @@ import { RecordModel, RecordSubscription } from 'pocketbase'
import { WritableAtom } from 'nanostores' import { WritableAtom } from 'nanostores'
import { timeDay, timeHour } from 'd3-time' import { timeDay, timeHour } from 'd3-time'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import useIsInViewport, { CallbackRef, HookOptions } from 'use-is-in-viewport'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
@@ -214,24 +213,6 @@ export function useYAxisWidth() {
return { yAxisWidth, updateYAxisWidth } return { yAxisWidth, updateYAxisWidth }
} }
export function useClampedIsInViewport(options: HookOptions): [boolean | null, CallbackRef] {
const [isInViewport, wrappedTargetRef] = useIsInViewport(options)
const [wasInViewportAtleastOnce, setWasInViewportAtleastOnce] = useState(isInViewport)
useEffect(() => {
setWasInViewportAtleastOnce((prev) => {
// this will clamp it to the first true
// received from useIsInViewport
if (!prev) {
return isInViewport
}
return prev
})
}, [isInViewport])
return [wasInViewportAtleastOnce, wrappedTargetRef]
}
export function toFixedWithoutTrailingZeros(num: number, digits: number) { export function toFixedWithoutTrailingZeros(num: number, digits: number) {
return parseFloat(num.toFixed(digits)).toString() return parseFloat(num.toFixed(digits)).toString()
} }

View File

@@ -43,6 +43,8 @@ export interface SystemStats {
mp: number mp: number
/** memory buffer + cache (gb) */ /** memory buffer + cache (gb) */
mb: number mb: number
/** zfs arc memory (gb) */
mz?: number
/** swap space (gb) */ /** swap space (gb) */
s: number s: number
/** swap used (gb) */ /** swap used (gb) */

View File

@@ -1,6 +1,6 @@
package beszel package beszel
const ( const (
Version = "0.5.0" Version = "0.5.2"
AppName = "beszel" AppName = "beszel"
) )

View File

@@ -105,20 +105,21 @@ 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] |
| `EXTRA_FILESYSTEMS` | unset | See [Monitoring additional disks / partitions](#monitoring-additional-disks--partitions) | | `EXTRA_FILESYSTEMS` | unset | See [Monitoring additional disks, partitions, or remote mounts](#monitoring-additional-disks-partitions-or-remote-mounts) |
| `FILESYSTEM` | unset | Device, partition, or mount point to use for root disk stats. | | `FILESYSTEM` | unset | Device, partition, or mount point to use for root disk 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. |
| `LOG_LEVEL` | info | Logging level. Valid values: "debug", "info", "warn", "error". | | `LOG_LEVEL` | info | Logging level. Valid values: "debug", "info", "warn", "error". |
| `NICS` | unset | Whitelist of network interfaces to monitor for bandwidth chart. | | `MEM_CALC` | unset | Overrides the default memory calculation.[^memcalc] |
| `PORT` | 45876 | Port or address:port to listen on. | | `NICS` | unset | Whitelist of network interfaces to monitor for bandwidth chart. |
| `SENSORS` | unset | Whitelist of temperature sensors to monitor. | | `PORT` | 45876 | Port or address:port to listen on. |
| `SENSORS` | unset | Whitelist of temperature sensors to monitor. |
<!-- | `SYS_SENSORS` | unset | Overrides the sys location for sensors. | --> | `SYS_SENSORS` | unset | Overrides sys path for sensors. See [#160](https://github.com/henrygd/beszel/discussions/160). |
[^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`.
[^memcalc]: The default value for used memory is based on gopsutil's [Used](https://pkg.go.dev/github.com/shirou/gopsutil/v4@v4.24.6/mem#VirtualMemoryStat) calculation, which should align fairly closely with `free`. Set `MEM_CALC` to `htop` to align with htop's calculation.
## OAuth / OIDC Setup ## OAuth / OIDC Setup
@@ -217,7 +218,7 @@ Assuming the agent is running, the connection is probably being blocked by a fir
1. Add an inbound rule to the agent system's firewall(s) to allow TCP connections to the port. Check any active firewalls, like iptables, and your cloud provider's firewall settings if applicable. 1. Add an inbound rule to the agent system's firewall(s) to allow TCP connections to the port. Check any active firewalls, like iptables, and your cloud provider's firewall settings if applicable.
2. Alternatively, use software like [Cloudflare Tunnel](https://www.cloudflare.com/products/tunnel/), [WireGuard](https://www.wireguard.com/), or [Tailscale](https://tailscale.com/) to securely bypass your firewall. 2. Alternatively, use software like [Cloudflare Tunnel](https://www.cloudflare.com/products/tunnel/), [WireGuard](https://www.wireguard.com/), or [Tailscale](https://tailscale.com/) to securely bypass your firewall.
You can test connectivity by running telnet `<agent-ip> <port>`. You can test connectivity by running `telnet <agent-ip> <port>`.
### Connecting the hub and agent on the same system using Docker ### Connecting the hub and agent on the same system using Docker